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