• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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(T activity, String labelString, Runnable onDismissed)69     public static <T extends Context & ActivityContext> void show(T activity, String labelString,
70             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().getString(labelStringResId),
80                 actionStringResId,
81                 onDismissed,
82                 onActionClicked);
83     }
84 
85     /** Show a snackbar with a label and action. */
show(T activity, String labelString, int actionStringResId, Runnable onDismissed, @Nullable Runnable onActionClicked)86     public static <T extends Context & ActivityContext> void show(T activity, String labelString,
87             int actionStringResId, Runnable onDismissed, @Nullable Runnable onActionClicked) {
88         closeOpenViews(activity, true, TYPE_SNACKBAR);
89         Snackbar snackbar = new Snackbar(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 = activity.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) + actionWidth)
147                 + labelView.getPaddingRight() + labelView.getPaddingLeft()
148                 + padding * 2;
149         if (totalContentWidth > params.width) {
150             // The text doesn't fit in our standard width so update width to accommodate.
151             if (totalContentWidth <= maxWidth) {
152                 params.width = totalContentWidth;
153             } else {
154                 // One line will be cut off, fallback to 2 lines and smaller font. (This should only
155                 // happen in some languages if system display and font size are set to largest.)
156                 int textHeight = res.getDimensionPixelSize(R.dimen.snackbar_content_height);
157                 float textSizePx = res.getDimension(R.dimen.snackbar_min_text_size);
158                 labelView.setLines(2);
159                 labelView.getLayoutParams().height = textHeight * 2;
160                 actionView.getLayoutParams().height = textHeight * 2;
161                 labelView.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSizePx);
162                 actionView.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSizePx);
163                 params.height += textHeight;
164                 params.width = maxWidth;
165             }
166         }
167 
168         snackbar.mOnDismissed = onDismissed;
169         snackbar.setAlpha(0);
170         snackbar.setScaleX(0.8f);
171         snackbar.setScaleY(0.8f);
172         snackbar.animate()
173                 .alpha(1f)
174                 .withLayer()
175                 .scaleX(1)
176                 .scaleY(1)
177                 .setDuration(SHOW_DURATION_MS)
178                 .setInterpolator(Interpolators.ACCELERATE_DECELERATE)
179                 .start();
180         int timeout = AccessibilityManagerCompat.getRecommendedTimeoutMillis(activity,
181                 TIMEOUT_DURATION_MS, FLAG_CONTENT_TEXT | FLAG_CONTENT_CONTROLS);
182         snackbar.postDelayed(() -> snackbar.close(true), timeout);
183     }
184 
185     @Override
handleClose(boolean animate)186     protected void handleClose(boolean animate) {
187         if (mIsOpen) {
188             if (animate) {
189                 animate().alpha(0f)
190                         .withLayer()
191                         .setStartDelay(0)
192                         .setDuration(HIDE_DURATION_MS)
193                         .setInterpolator(Interpolators.ACCELERATE)
194                         .withEndAction(this::onClosed)
195                         .start();
196             } else {
197                 animate().cancel();
198                 onClosed();
199             }
200             mIsOpen = false;
201         }
202     }
203 
onClosed()204     private void onClosed() {
205         mActivity.getDragLayer().removeView(this);
206         if (mOnDismissed != null) {
207             mOnDismissed.run();
208         }
209     }
210 
211     @Override
isOfType(int type)212     protected boolean isOfType(int type) {
213         return (type & TYPE_SNACKBAR) != 0;
214     }
215 
216     @Override
onControllerInterceptTouchEvent(MotionEvent ev)217     public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
218         if (ev.getAction() == MotionEvent.ACTION_DOWN) {
219             BaseDragLayer dl = mActivity.getDragLayer();
220             if (!dl.isEventOverView(this, ev)) {
221                 close(true);
222             }
223         }
224         return false;
225     }
226 }
227