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.app.animation.Interpolators;
34 import com.android.launcher3.AbstractFloatingView;
35 import com.android.launcher3.DeviceProfile;
36 import com.android.launcher3.R;
37 import com.android.launcher3.compat.AccessibilityManagerCompat;
38 import com.android.launcher3.dragndrop.DragLayer;
39 
40 /**
41  * A toast-like UI at the bottom of the screen with a label, button action, and dismiss action.
42  */
43 public class Snackbar extends AbstractFloatingView {
44 
45     private static final long SHOW_DURATION_MS = 180;
46     private static final long HIDE_DURATION_MS = 180;
47     private static final int TIMEOUT_DURATION_MS = 4000;
48 
49     private final ActivityContext mActivity;
50     private Runnable mOnDismissed;
51 
Snackbar(Context context, AttributeSet attrs)52     public Snackbar(Context context, AttributeSet attrs) {
53         this(context, attrs, 0);
54     }
55 
Snackbar(Context context, AttributeSet attrs, int defStyleAttr)56     public Snackbar(Context context, AttributeSet attrs, int defStyleAttr) {
57         super(context, attrs, defStyleAttr);
58         mActivity = ActivityContext.lookupContext(context);
59         inflate(context, R.layout.snackbar, this);
60     }
61 
62     /** Show a snackbar with just a label. */
show(T activity, int labelStringRedId, Runnable onDismissed)63     public static <T extends Context & ActivityContext> void show(T activity, int labelStringRedId,
64             Runnable onDismissed) {
65         show(activity, labelStringRedId, NO_ID, onDismissed, null);
66     }
67 
68     /** Show a snackbar with just a label. */
show( ActivityContext activity, CharSequence labelString, Runnable onDismissed)69     public static void show(
70             ActivityContext activity, CharSequence labelString, Runnable onDismissed) {
71         show(activity, labelString, NO_ID, onDismissed, null);
72     }
73 
74     /** Show a snackbar with a label and action. */
show(T activity, int labelStringResId, int actionStringResId, Runnable onDismissed, @Nullable Runnable onActionClicked)75     public static <T extends Context & ActivityContext> void show(T activity, int labelStringResId,
76             int actionStringResId, Runnable onDismissed, @Nullable Runnable onActionClicked) {
77         show(
78                 activity,
79                 activity.getResources().getText(labelStringResId),
80                 actionStringResId,
81                 onDismissed,
82                 onActionClicked);
83     }
84 
85     /** Show a snackbar with a label and action. */
show(ActivityContext activity, CharSequence labelString, int actionStringResId, Runnable onDismissed, @Nullable Runnable onActionClicked)86     public static void show(ActivityContext activity, CharSequence labelString,
87             int actionStringResId, Runnable onDismissed, @Nullable Runnable onActionClicked) {
88         closeOpenViews(activity, true, TYPE_SNACKBAR);
89         Snackbar snackbar = new Snackbar((Context) activity, null);
90         // Set some properties here since inflated xml only contains the children.
91         snackbar.setOrientation(HORIZONTAL);
92         snackbar.setGravity(Gravity.CENTER_VERTICAL);
93         Resources res = snackbar.getResources();
94         snackbar.setElevation(res.getDimension(R.dimen.snackbar_elevation));
95         int padding = res.getDimensionPixelSize(R.dimen.snackbar_padding);
96         snackbar.setPadding(padding, padding, padding, padding);
97         snackbar.setBackgroundResource(R.drawable.round_rect_primary);
98 
99         snackbar.mIsOpen = true;
100         BaseDragLayer dragLayer = activity.getDragLayer();
101         dragLayer.addView(snackbar);
102 
103         DragLayer.LayoutParams params = (DragLayer.LayoutParams) snackbar.getLayoutParams();
104         params.gravity = Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM;
105         params.height = res.getDimensionPixelSize(R.dimen.snackbar_height);
106         int maxMarginLeftRight = res.getDimensionPixelSize(R.dimen.snackbar_max_margin_left_right);
107         int minMarginLeftRight = res.getDimensionPixelSize(R.dimen.snackbar_min_margin_left_right);
108         int marginBottom = res.getDimensionPixelSize(R.dimen.snackbar_margin_bottom);
109         int absoluteMaxWidth = res.getDimensionPixelSize(R.dimen.snackbar_max_width);
110         Rect insets = activity.getDeviceProfile().getInsets();
111         int maxWidth = Math.min(
112                 dragLayer.getWidth() - minMarginLeftRight * 2 - insets.left - insets.right,
113                 absoluteMaxWidth);
114         int minWidth = Math.min(
115                 dragLayer.getWidth() - maxMarginLeftRight * 2 - insets.left - insets.right,
116                 absoluteMaxWidth);
117         params.width = minWidth;
118         DeviceProfile deviceProfile = activity.getDeviceProfile();
119         params.setMargins(0, 0, 0, marginBottom
120                 + (deviceProfile.isTaskbarPresent
121                 ? deviceProfile.taskbarHeight + deviceProfile.getTaskbarOffsetY()
122                 : insets.bottom));
123 
124         TextView labelView = snackbar.findViewById(R.id.label);
125         labelView.setText(labelString);
126 
127         TextView actionView = snackbar.findViewById(R.id.action);
128         float actionWidth;
129         if (actionStringResId != NO_ID) {
130             String actionText = res.getString(actionStringResId);
131             actionWidth = actionView.getPaint().measureText(actionText)
132                     + actionView.getPaddingRight() + actionView.getPaddingLeft();
133             actionView.setText(actionText);
134             actionView.setOnClickListener(v -> {
135                 if (onActionClicked != null) {
136                     onActionClicked.run();
137                 }
138                 snackbar.mOnDismissed = null;
139                 snackbar.close(true);
140             });
141         } else {
142             actionWidth = 0;
143             actionView.setVisibility(GONE);
144         }
145 
146         int totalContentWidth = (int) (labelView.getPaint().measureText(labelString.toString())
147                     + actionWidth)
148                 + labelView.getPaddingRight() + labelView.getPaddingLeft()
149                 + padding * 2;
150         if (totalContentWidth > params.width) {
151             // The text doesn't fit in our standard width so update width to accommodate.
152             if (totalContentWidth <= maxWidth) {
153                 params.width = totalContentWidth;
154             } else {
155                 // One line will be cut off, fallback to 2 lines and smaller font. (This should only
156                 // happen in some languages if system display and font size are set to largest.)
157                 int textHeight = res.getDimensionPixelSize(R.dimen.snackbar_content_height);
158                 float textSizePx = res.getDimension(R.dimen.snackbar_min_text_size);
159                 labelView.setLines(2);
160                 labelView.getLayoutParams().height = textHeight * 2;
161                 actionView.getLayoutParams().height = textHeight * 2;
162                 labelView.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSizePx);
163                 actionView.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSizePx);
164                 params.height += textHeight;
165                 params.width = maxWidth;
166             }
167         }
168 
169         snackbar.mOnDismissed = onDismissed;
170         snackbar.setAlpha(0);
171         snackbar.setScaleX(0.8f);
172         snackbar.setScaleY(0.8f);
173         snackbar.animate()
174                 .alpha(1f)
175                 .withLayer()
176                 .scaleX(1)
177                 .scaleY(1)
178                 .setDuration(SHOW_DURATION_MS)
179                 .setInterpolator(Interpolators.ACCELERATE_DECELERATE)
180                 .start();
181         int timeout = AccessibilityManagerCompat.getRecommendedTimeoutMillis(snackbar.getContext(),
182                 TIMEOUT_DURATION_MS, FLAG_CONTENT_TEXT | FLAG_CONTENT_CONTROLS);
183         snackbar.postDelayed(() -> snackbar.close(true), timeout);
184     }
185 
186     @Override
handleClose(boolean animate)187     protected void handleClose(boolean animate) {
188         if (mIsOpen) {
189             if (animate) {
190                 animate().alpha(0f)
191                         .withLayer()
192                         .setStartDelay(0)
193                         .setDuration(HIDE_DURATION_MS)
194                         .setInterpolator(Interpolators.ACCELERATE)
195                         .withEndAction(this::onClosed)
196                         .start();
197             } else {
198                 animate().cancel();
199                 onClosed();
200             }
201             mIsOpen = false;
202         }
203     }
204 
onClosed()205     private void onClosed() {
206         mActivity.getDragLayer().removeView(this);
207         if (mOnDismissed != null) {
208             mOnDismissed.run();
209         }
210     }
211 
212     @Override
isOfType(int type)213     protected boolean isOfType(int type) {
214         return (type & TYPE_SNACKBAR) != 0;
215     }
216 
217     @Override
onControllerInterceptTouchEvent(MotionEvent ev)218     public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
219         if (ev.getAction() == MotionEvent.ACTION_DOWN) {
220             BaseDragLayer dl = mActivity.getDragLayer();
221             if (!dl.isEventOverView(this, ev)) {
222                 close(true);
223             }
224         }
225         return false;
226     }
227 }
228