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