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