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