/* * Copyright (C) 2018 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.launcher3.views; import static android.view.accessibility.AccessibilityManager.FLAG_CONTENT_CONTROLS; import static android.view.accessibility.AccessibilityManager.FLAG_CONTENT_TEXT; import android.content.Context; import android.content.res.Resources; import android.graphics.Rect; import android.util.AttributeSet; import android.util.TypedValue; import android.view.Gravity; import android.view.MotionEvent; import android.widget.TextView; import com.android.launcher3.AbstractFloatingView; import com.android.launcher3.Launcher; import com.android.launcher3.R; import com.android.launcher3.anim.Interpolators; import com.android.launcher3.compat.AccessibilityManagerCompat; import com.android.launcher3.dragndrop.DragLayer; /** * A toast-like UI at the bottom of the screen with a label, button action, and dismiss action. */ public class Snackbar extends AbstractFloatingView { private static final long SHOW_DURATION_MS = 180; private static final long HIDE_DURATION_MS = 180; private static final int TIMEOUT_DURATION_MS = 4000; private final Launcher mLauncher; private Runnable mOnDismissed; public Snackbar(Context context, AttributeSet attrs) { this(context, attrs, 0); } public Snackbar(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); mLauncher = Launcher.getLauncher(context); inflate(context, R.layout.snackbar, this); } public static void show(Launcher launcher, int labelStringResId, int actionStringResId, Runnable onDismissed, Runnable onActionClicked) { closeOpenViews(launcher, true, TYPE_SNACKBAR); Snackbar snackbar = new Snackbar(launcher, null); // Set some properties here since inflated xml only contains the children. snackbar.setOrientation(HORIZONTAL); snackbar.setGravity(Gravity.CENTER_VERTICAL); Resources res = launcher.getResources(); snackbar.setElevation(res.getDimension(R.dimen.snackbar_elevation)); int padding = res.getDimensionPixelSize(R.dimen.snackbar_padding); snackbar.setPadding(padding, padding, padding, padding); snackbar.setBackgroundResource(R.drawable.round_rect_primary); snackbar.mIsOpen = true; DragLayer dragLayer = launcher.getDragLayer(); dragLayer.addView(snackbar); DragLayer.LayoutParams params = (DragLayer.LayoutParams) snackbar.getLayoutParams(); params.gravity = Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM; params.height = res.getDimensionPixelSize(R.dimen.snackbar_height); int maxMarginLeftRight = res.getDimensionPixelSize(R.dimen.snackbar_max_margin_left_right); int minMarginLeftRight = res.getDimensionPixelSize(R.dimen.snackbar_min_margin_left_right); int marginBottom = res.getDimensionPixelSize(R.dimen.snackbar_margin_bottom); Rect insets = launcher.getDeviceProfile().getInsets(); int maxWidth = dragLayer.getWidth() - minMarginLeftRight * 2 - insets.left - insets.right; int minWidth = dragLayer.getWidth() - maxMarginLeftRight * 2 - insets.left - insets.right; params.width = minWidth; params.setMargins(0, 0, 0, marginBottom + insets.bottom); TextView labelView = snackbar.findViewById(R.id.label); TextView actionView = snackbar.findViewById(R.id.action); String labelText = res.getString(labelStringResId); String actionText = res.getString(actionStringResId); int totalContentWidth = (int) (labelView.getPaint().measureText(labelText) + actionView.getPaint().measureText(actionText)) + labelView.getPaddingRight() + labelView.getPaddingLeft() + actionView.getPaddingRight() + actionView.getPaddingLeft() + padding * 2; if (totalContentWidth > params.width) { // The text doesn't fit in our standard width so update width to accommodate. if (totalContentWidth <= maxWidth) { params.width = totalContentWidth; } else { // One line will be cut off, fallback to 2 lines and smaller font. (This should only // happen in some languages if system display and font size are set to largest.) int textHeight = res.getDimensionPixelSize(R.dimen.snackbar_content_height); float textSizePx = res.getDimension(R.dimen.snackbar_min_text_size); labelView.setLines(2); labelView.getLayoutParams().height = textHeight * 2; actionView.getLayoutParams().height = textHeight * 2; labelView.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSizePx); actionView.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSizePx); params.height += textHeight; params.width = maxWidth; } } labelView.setText(labelText); actionView.setText(actionText); actionView.setOnClickListener(v -> { if (onActionClicked != null) { onActionClicked.run(); } snackbar.mOnDismissed = null; snackbar.close(true); }); snackbar.mOnDismissed = onDismissed; snackbar.setAlpha(0); snackbar.setScaleX(0.8f); snackbar.setScaleY(0.8f); snackbar.animate() .alpha(1f) .withLayer() .scaleX(1) .scaleY(1) .setDuration(SHOW_DURATION_MS) .setInterpolator(Interpolators.ACCEL_DEACCEL) .start(); int timeout = AccessibilityManagerCompat.getRecommendedTimeoutMillis(launcher, TIMEOUT_DURATION_MS, FLAG_CONTENT_TEXT | FLAG_CONTENT_CONTROLS); snackbar.postDelayed(() -> snackbar.close(true), timeout); } @Override protected void handleClose(boolean animate) { if (mIsOpen) { if (animate) { animate().alpha(0f) .withLayer() .setStartDelay(0) .setDuration(HIDE_DURATION_MS) .setInterpolator(Interpolators.ACCEL) .withEndAction(this::onClosed) .start(); } else { animate().cancel(); onClosed(); } mIsOpen = false; } } private void onClosed() { mLauncher.getDragLayer().removeView(this); if (mOnDismissed != null) { mOnDismissed.run(); } } @Override public void logActionCommand(int command) { // TODO } @Override protected boolean isOfType(int type) { return (type & TYPE_SNACKBAR) != 0; } @Override public boolean onControllerInterceptTouchEvent(MotionEvent ev) { if (ev.getAction() == MotionEvent.ACTION_DOWN) { DragLayer dl = mLauncher.getDragLayer(); if (!dl.isEventOverView(this, ev)) { close(true); } } return false; } }