blob: bc707afd68f505c0b5594612c883f7ab9f0ce108 [file] [log] [blame]
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.components.browser_ui.widget;
import android.app.Activity;
import android.graphics.Color;
import android.graphics.Rect;
import android.graphics.drawable.ColorDrawable;
import android.os.Build;
import android.provider.Settings;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnLayoutChangeListener;
import android.view.ViewGroup.LayoutParams;
import android.view.Window;
import android.view.WindowManager;
import android.view.animation.Animation;
import android.view.animation.Animation.AnimationListener;
import android.view.animation.ScaleAnimation;
import android.widget.FrameLayout;
import androidx.annotation.Nullable;
import org.chromium.base.ApiCompatibilityUtils;
import org.chromium.base.ContextUtils;
import org.chromium.components.browser_ui.widget.animation.Interpolators;
import org.chromium.ui.UiUtils;
import org.chromium.ui.util.ColorUtils;
import org.chromium.ui.widget.AnchoredPopupWindow;
import org.chromium.ui.widget.RectProvider;
/**
* ContextMenuDialog is a subclass of AlwaysDismissedDialog that ensures that the proper scale
* animation is played upon calling {@link #show()} and {@link #dismiss()}.
*/
public class ContextMenuDialog extends AlwaysDismissedDialog {
public static final int NO_CUSTOM_MARGIN = -1;
private static final long ENTER_ANIMATION_DURATION_MS = 250;
// Exit animation duration should be set to 60% of the enter animation duration.
private static final long EXIT_ANIMATION_DURATION_MS = 150;
private final Activity mActivity;
private final View mContentView;
private final float mTouchPointXPx;
private final float mTouchPointYPx;
private final float mTopContentOffsetPx;
private final boolean mIsPopup;
private final boolean mShouldRemoveScrim;
private float mContextMenuSourceXPx;
private float mContextMenuSourceYPx;
private int mContextMenuFirstLocationYPx;
private @Nullable AnchoredPopupWindow mPopupWindow;
private View mLayout;
private OnLayoutChangeListener mOnLayoutChangeListener;
private int mTopMarginPx;
private int mBottomMarginPx;
private Integer mPopupMargin;
private Integer mDesiredPopupContentWidth;
/**
* Creates an instance of the ContextMenuDialog.
* @param ownerActivity The activity in which the dialog should run
* @param theme A style resource describing the theme to use for the window, or {@code 0} to use
* the default dialog theme
* @param touchPointXPx The x-coordinate of the touch that triggered the context menu.
* @param touchPointYPx The y-coordinate of the touch that triggered the context menu.
* @param topContentOffsetPx The offset of the content from the top.
* @param topMarginPx An explicit top margin for the dialog, or -1 to use default
* defined in XML.
* @param bottomMarginPx An explicit bottom margin for the dialog, or -1 to use default
* defined in XML.
* @param layout The context menu layout that will house the menu.
* @param contentView The context menu view to display on the dialog.
* @param isPopup Whether the context menu is being shown in a {@link AnchoredPopupWindow}.
* @param shouldRemoveScrim Whether the context menu should removes the scrim behind the dialog
* visually.
* @param popupMargin The margin for the context menu.
* @param desiredPopupContentWidth The desired width for the content of the context menu.
*/
public ContextMenuDialog(Activity ownerActivity, int theme, float touchPointXPx,
float touchPointYPx, float topContentOffsetPx, int topMarginPx, int bottomMarginPx,
View layout, View contentView, boolean isPopup, boolean shouldRemoveScrim,
@Nullable Integer popupMargin, @Nullable Integer desiredPopupContentWidth) {
super(ownerActivity, theme);
mActivity = ownerActivity;
mTouchPointXPx = touchPointXPx;
mTouchPointYPx = touchPointYPx;
mTopContentOffsetPx = topContentOffsetPx;
mTopMarginPx = topMarginPx;
mBottomMarginPx = bottomMarginPx;
mContentView = contentView;
mLayout = layout;
mIsPopup = isPopup;
mShouldRemoveScrim = shouldRemoveScrim;
mPopupMargin = popupMargin;
mDesiredPopupContentWidth = desiredPopupContentWidth;
}
@Override
public void onStart() {
super.onStart();
Window dialogWindow = getWindow();
dialogWindow.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
dialogWindow.setLayout(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
if (mShouldRemoveScrim) {
dialogWindow.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND);
dialogWindow.addFlags(WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL);
dialogWindow.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
// Set the navigation bar when API level >= 27 to match android:navigationBarColor
// reference in styles.xml.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
dialogWindow.setNavigationBarColor(mActivity.getWindow().getNavigationBarColor());
UiUtils.setNavigationBarIconColor(dialogWindow.getDecorView(),
mActivity.getResources().getBoolean(R.bool.window_light_navigation_bar));
}
// Apply the status bar color in case the website had override them.
ApiCompatibilityUtils.setStatusBarColor(
dialogWindow, mActivity.getWindow().getStatusBarColor());
ApiCompatibilityUtils.setStatusBarIconColor(dialogWindow.getDecorView().getRootView(),
!ColorUtils.shouldUseLightForegroundOnBackground(
mActivity.getWindow().getStatusBarColor()));
}
// Both bottom margin and top margin must be set together to ensure default
// values are not relied upon for custom behavior.
if (mTopMarginPx != NO_CUSTOM_MARGIN && mBottomMarginPx != NO_CUSTOM_MARGIN) {
// TODO(benwgold): Update to relative layout to avoid have to set fixed margin.
FrameLayout.LayoutParams layoutParams =
(FrameLayout.LayoutParams) mContentView.getLayoutParams();
if (layoutParams == null) return;
layoutParams.bottomMargin = mBottomMarginPx;
layoutParams.topMargin = mTopMarginPx;
}
mOnLayoutChangeListener = new OnLayoutChangeListener() {
@Override
public void onLayoutChange(View v, int left, int top, int right, int bottom,
int oldLeft, int oldTop, int oldRight, int oldBottom) {
// // If the layout size does not change (e.g. call due to #forceLayout), do nothing
// // because we don't want to dismiss the context menu.
if (left == oldLeft && right == oldRight && top == oldTop && bottom == oldBottom) {
return;
}
if (mIsPopup) {
// If the menu is a popup, wait for the layout to be measured, then proceed with
// showing the popup window.
if (v.getMeasuredHeight() == 0) return;
// If dialog is showing and the layout changes, we might lost the anchor point.
// We'll dismiss the context menu and remove the listener.
if (mPopupWindow != null && mPopupWindow.isShowing()) {
dismiss();
return;
}
final int posX = (int) mTouchPointXPx;
final int posY = (int) (mTouchPointYPx + mTopContentOffsetPx);
final Rect rect = new Rect(posX, posY, posX, posY);
mPopupWindow = new AnchoredPopupWindow(mActivity, mLayout,
new ColorDrawable(Color.TRANSPARENT), mContentView,
new RectProvider(rect));
if (mPopupMargin != null) {
mPopupWindow.setMargin(mPopupMargin);
}
if (mDesiredPopupContentWidth != null) {
mPopupWindow.setDesiredContentWidth(mDesiredPopupContentWidth);
}
mPopupWindow.setOutsideTouchable(false);
mPopupWindow.show();
} else {
// Otherwise, the menu will already be in the hierarchy, and we need to make
// sure the menu itself is measured before starting the animation.
if (v.getMeasuredHeight() == 0) return;
startEnterAnimation();
v.removeOnLayoutChangeListener(this);
mOnLayoutChangeListener = null;
}
}
};
(mIsPopup ? mLayout : mContentView).addOnLayoutChangeListener(mOnLayoutChangeListener);
}
private void startEnterAnimation() {
Rect rectangle = new Rect();
Window window = mActivity.getWindow();
window.getDecorView().getWindowVisibleDisplayFrame(rectangle);
float xOffsetPx = rectangle.left;
float yOffsetPx = rectangle.top + mTopContentOffsetPx;
int[] currentLocationOnScreenPx = new int[2];
mContentView.getLocationOnScreen(currentLocationOnScreenPx);
mContextMenuFirstLocationYPx = currentLocationOnScreenPx[1];
mContextMenuSourceXPx = mTouchPointXPx - currentLocationOnScreenPx[0] + xOffsetPx;
mContextMenuSourceYPx = mTouchPointYPx - currentLocationOnScreenPx[1] + yOffsetPx;
Animation animation = getScaleAnimation(true, mContextMenuSourceXPx, mContextMenuSourceYPx);
mContentView.startAnimation(animation);
}
@Override
public void dismiss() {
if (mIsPopup) {
if (mPopupWindow != null) {
mPopupWindow.dismiss();
mPopupWindow = null;
}
if (mOnLayoutChangeListener != null) {
mLayout.removeOnLayoutChangeListener(mOnLayoutChangeListener);
mOnLayoutChangeListener = null;
}
super.dismiss();
return;
}
if (mOnLayoutChangeListener != null) {
mContentView.removeOnLayoutChangeListener(mOnLayoutChangeListener);
mOnLayoutChangeListener = null;
}
int[] contextMenuFinalLocationPx = new int[2];
mContentView.getLocationOnScreen(contextMenuFinalLocationPx);
// Recalculate mContextMenuDestinationY because the context menu's final location may not be
// the same as its first location if it changed in height.
float contextMenuDestinationYPx = mContextMenuSourceYPx
+ (mContextMenuFirstLocationYPx - contextMenuFinalLocationPx[1]);
Animation exitAnimation =
getScaleAnimation(false, mContextMenuSourceXPx, contextMenuDestinationYPx);
exitAnimation.setAnimationListener(new AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {}
@Override
public void onAnimationRepeat(Animation animation) {}
@Override
public void onAnimationEnd(Animation animation) {
ContextMenuDialog.super.dismiss();
}
});
mContentView.startAnimation(exitAnimation);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
dismiss();
return true;
}
return false;
}
/**
* @param isEnterAnimation Whether the animation to be returned is for showing the context menu
* as opposed to hiding it.
* @param pivotX The X coordinate of the point about which the object is being scaled, specified
* as an absolute number where 0 is the left edge.
* @param pivotY The Y coordinate of the point about which the object is being scaled, specified
* as an absolute number where 0 is the top edge.
* @return Returns the scale animation for the context menu.
*/
public Animation getScaleAnimation(boolean isEnterAnimation, float pivotX, float pivotY) {
float fromX = isEnterAnimation ? 0f : 1f;
float toX = isEnterAnimation ? 1f : 0f;
float fromY = fromX;
float toY = toX;
ScaleAnimation animation = new ScaleAnimation(
fromX, toX, fromY, toY, Animation.ABSOLUTE, pivotX, Animation.ABSOLUTE, pivotY);
long duration = isEnterAnimation ? ENTER_ANIMATION_DURATION_MS : EXIT_ANIMATION_DURATION_MS;
float durationScale =
Settings.Global.getFloat(ContextUtils.getApplicationContext().getContentResolver(),
Settings.Global.ANIMATOR_DURATION_SCALE, 1f);
animation.setDuration((long) (duration * durationScale));
animation.setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN_INTERPOLATOR);
return animation;
}
}