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