1 /*
2  * Copyright (C) 2018 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.launcher3.views;
18 
19 import static android.view.accessibility.AccessibilityManager.FLAG_CONTENT_CONTROLS;
20 import static android.view.accessibility.AccessibilityManager.FLAG_CONTENT_TEXT;
21 
22 import android.content.Context;
23 import android.content.res.Resources;
24 import android.graphics.Rect;
25 import android.util.AttributeSet;
26 import android.util.TypedValue;
27 import android.view.Gravity;
28 import android.view.MotionEvent;
29 import android.widget.TextView;
30 
31 import androidx.annotation.Nullable;
32 
33 import com.android.launcher3.AbstractFloatingView;
34 import com.android.launcher3.R;
35 import com.android.launcher3.anim.Interpolators;
36 import com.android.launcher3.compat.AccessibilityManagerCompat;
37 import com.android.launcher3.dragndrop.DragLayer;
38 
39 /**
40  * A toast-like UI at the bottom of the screen with a label, button action, and dismiss action.
41  */
42 public class Snackbar extends AbstractFloatingView {
43 
44     private static final long SHOW_DURATION_MS = 180;
45     private static final long HIDE_DURATION_MS = 180;
46     private static final int TIMEOUT_DURATION_MS = 4000;
47 
48     private final ActivityContext mActivity;
49     private Runnable mOnDismissed;
50 
Snackbar(Context context, AttributeSet attrs)51     public Snackbar(Context context, AttributeSet attrs) {
52         this(context, attrs, 0);
53     }
54 
Snackbar(Context context, AttributeSet attrs, int defStyleAttr)55     public Snackbar(Context context, AttributeSet attrs, int defStyleAttr) {
56         super(context, attrs, defStyleAttr);
57         mActivity = ActivityContext.lookupContext(context);
58         inflate(context, R.layout.snackbar, this);
59     }
60 
61     /** Show a snackbar with just a label. */
show(T activity, int labelStringRedId, Runnable onDismissed)62     public static <T extends Context & ActivityContext> void show(T activity, int labelStringRedId,
63             Runnable onDismissed) {
64         show(activity, labelStringRedId, NO_ID, onDismissed, null);
65     }
66 
67     /** Show a snackbar with a label and action. */
show(T activity, int labelStringResId, int actionStringResId, Runnable onDismissed, @Nullable Runnable onActionClicked)68     public static <T extends Context & ActivityContext> void show(T activity, int labelStringResId,
69             int actionStringResId, Runnable onDismissed, @Nullable Runnable onActionClicked) {
70         closeOpenViews(activity, true, TYPE_SNACKBAR);
71         Snackbar snackbar = new Snackbar(activity, null);
72         // Set some properties here since inflated xml only contains the children.
73         snackbar.setOrientation(HORIZONTAL);
74         snackbar.setGravity(Gravity.CENTER_VERTICAL);
75         Resources res = activity.getResources();
76         snackbar.setElevation(res.getDimension(R.dimen.snackbar_elevation));
77         int padding = res.getDimensionPixelSize(R.dimen.snackbar_padding);
78         snackbar.setPadding(padding, padding, padding, padding);
79         snackbar.setBackgroundResource(R.drawable.round_rect_primary);
80 
81         snackbar.mIsOpen = true;
82         BaseDragLayer dragLayer = activity.getDragLayer();
83         dragLayer.addView(snackbar);
84 
85         DragLayer.LayoutParams params = (DragLayer.LayoutParams) snackbar.getLayoutParams();
86         params.gravity = Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM;
87         params.height = res.getDimensionPixelSize(R.dimen.snackbar_height);
88         int maxMarginLeftRight = res.getDimensionPixelSize(R.dimen.snackbar_max_margin_left_right);
89         int minMarginLeftRight = res.getDimensionPixelSize(R.dimen.snackbar_min_margin_left_right);
90         int marginBottom = res.getDimensionPixelSize(R.dimen.snackbar_margin_bottom);
91         int absoluteMaxWidth = res.getDimensionPixelSize(R.dimen.snackbar_max_width);
92         Rect insets = activity.getDeviceProfile().getInsets();
93         int maxWidth = Math.min(
94                 dragLayer.getWidth() - minMarginLeftRight * 2 - insets.left - insets.right,
95                 absoluteMaxWidth);
96         int minWidth = Math.min(
97                 dragLayer.getWidth() - maxMarginLeftRight * 2 - insets.left - insets.right,
98                 absoluteMaxWidth);
99         params.width = minWidth;
100         params.setMargins(0, 0, 0, marginBottom + insets.bottom);
101 
102         TextView labelView = snackbar.findViewById(R.id.label);
103         String labelText = res.getString(labelStringResId);
104         labelView.setText(labelText);
105 
106         TextView actionView = snackbar.findViewById(R.id.action);
107         float actionWidth;
108         if (actionStringResId != NO_ID) {
109             String actionText = res.getString(actionStringResId);
110             actionWidth = actionView.getPaint().measureText(actionText)
111                     + actionView.getPaddingRight() + actionView.getPaddingLeft();
112             actionView.setText(actionText);
113             actionView.setOnClickListener(v -> {
114                 if (onActionClicked != null) {
115                     onActionClicked.run();
116                 }
117                 snackbar.mOnDismissed = null;
118                 snackbar.close(true);
119             });
120         } else {
121             actionWidth = 0;
122             actionView.setVisibility(GONE);
123         }
124 
125         int totalContentWidth = (int) (labelView.getPaint().measureText(labelText) + actionWidth)
126                 + labelView.getPaddingRight() + labelView.getPaddingLeft()
127                 + padding * 2;
128         if (totalContentWidth > params.width) {
129             // The text doesn't fit in our standard width so update width to accommodate.
130             if (totalContentWidth <= maxWidth) {
131                 params.width = totalContentWidth;
132             } else {
133                 // One line will be cut off, fallback to 2 lines and smaller font. (This should only
134                 // happen in some languages if system display and font size are set to largest.)
135                 int textHeight = res.getDimensionPixelSize(R.dimen.snackbar_content_height);
136                 float textSizePx = res.getDimension(R.dimen.snackbar_min_text_size);
137                 labelView.setLines(2);
138                 labelView.getLayoutParams().height = textHeight * 2;
139                 actionView.getLayoutParams().height = textHeight * 2;
140                 labelView.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSizePx);
141                 actionView.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSizePx);
142                 params.height += textHeight;
143                 params.width = maxWidth;
144             }
145         }
146 
147         snackbar.mOnDismissed = onDismissed;
148         snackbar.setAlpha(0);
149         snackbar.setScaleX(0.8f);
150         snackbar.setScaleY(0.8f);
151         snackbar.animate()
152                 .alpha(1f)
153                 .withLayer()
154                 .scaleX(1)
155                 .scaleY(1)
156                 .setDuration(SHOW_DURATION_MS)
157                 .setInterpolator(Interpolators.ACCEL_DEACCEL)
158                 .start();
159         int timeout = AccessibilityManagerCompat.getRecommendedTimeoutMillis(activity,
160                 TIMEOUT_DURATION_MS, FLAG_CONTENT_TEXT | FLAG_CONTENT_CONTROLS);
161         snackbar.postDelayed(() -> snackbar.close(true), timeout);
162     }
163 
164     @Override
handleClose(boolean animate)165     protected void handleClose(boolean animate) {
166         if (mIsOpen) {
167             if (animate) {
168                 animate().alpha(0f)
169                         .withLayer()
170                         .setStartDelay(0)
171                         .setDuration(HIDE_DURATION_MS)
172                         .setInterpolator(Interpolators.ACCEL)
173                         .withEndAction(this::onClosed)
174                         .start();
175             } else {
176                 animate().cancel();
177                 onClosed();
178             }
179             mIsOpen = false;
180         }
181     }
182 
onClosed()183     private void onClosed() {
184         mActivity.getDragLayer().removeView(this);
185         if (mOnDismissed != null) {
186             mOnDismissed.run();
187         }
188     }
189 
190     @Override
isOfType(int type)191     protected boolean isOfType(int type) {
192         return (type & TYPE_SNACKBAR) != 0;
193     }
194 
195     @Override
onControllerInterceptTouchEvent(MotionEvent ev)196     public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
197         if (ev.getAction() == MotionEvent.ACTION_DOWN) {
198             BaseDragLayer dl = mActivity.getDragLayer();
199             if (!dl.isEventOverView(this, ev)) {
200                 close(true);
201             }
202         }
203         return false;
204     }
205 }
206