blob: 248e16ddf6d44bd1eb9a3100b1c6d9241a08bb7a [file] [log] [blame]
// Copyright 2018 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.chrome.browser.page_info;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.app.Dialog;
import android.content.Context;
import android.content.DialogInterface;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.support.annotation.NonNull;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import android.view.Window;
import android.widget.LinearLayout;
import android.widget.ScrollView;
import org.chromium.ui.interpolators.BakedBezierInterpolator;
import org.chromium.ui.modaldialog.DialogDismissalCause;
import org.chromium.ui.modaldialog.ModalDialogManager;
import org.chromium.ui.modaldialog.ModalDialogProperties;
import org.chromium.ui.modelutil.PropertyModel;
/**
* Represents the dialog containing the page info view.
*/
class PageInfoDialog {
private static final int ENTER_START_DELAY_MS = 100;
private static final int ENTER_EXIT_DURATION_MS = 200;
private static final int CLOSE_CLEANUP_DELAY_MS = 10;
@NonNull
private final PageInfoView mView;
private final boolean mIsSheet;
// The dialog implementation.
// mSheetDialog is set if the dialog appears as a sheet. Otherwise, mModalDialog is set.
private final Dialog mSheetDialog;
private final PropertyModel mModalDialogModel;
@NonNull
private final ModalDialogManager mManager;
@NonNull
private final ModalDialogProperties.Controller mController;
// Animation which is currently running, if there is one.
private Animator mCurrentAnimation;
private boolean mDismissWithoutAnimation;
/**
* Creates a new page info dialog. The dialog can appear as a sheet (using Android dialogs) or a
* standard dialog (using modal dialogs).
*
* @param context The context used for creating the dialog.
* @param view The view shown inside the dialog.
* @param tabView The view if the tab the dialog is shown in.
* @param isSheet Whether the dialog should appear as a sheet.
* @param manager The dialog's manager used for modal dialogs.
* @param controller The dialog's controller.
*
*/
public PageInfoDialog(Context context, @NonNull PageInfoView view, View tabView,
boolean isSheet, @NonNull ModalDialogManager manager,
@NonNull ModalDialogProperties.Controller controller) {
mView = view;
mIsSheet = isSheet;
mManager = manager;
mController = controller;
mView.setVisibility(View.INVISIBLE);
mView.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
@Override
public void onLayoutChange(
View v, int l, int t, int r, int b, int ol, int ot, int or, int ob) {
// Trigger the entrance animations once the main container has been laid out and has
// a height.
mView.removeOnLayoutChangeListener(this);
mView.setVisibility(View.VISIBLE);
createAllAnimations(true, null).start();
}
});
ViewGroup container;
if (isSheet) {
// On smaller screens, make the dialog fill the width of the screen.
container = createSheetContainer(context, tabView);
} else {
// On larger screens, modal dialog already has an maximum width set.
container = new ScrollView(context);
}
container.addView(mView);
if (isSheet) {
mSheetDialog = createSheetDialog(context, container);
mModalDialogModel = null;
} else {
mModalDialogModel = createModalDialog(container);
mSheetDialog = null;
}
}
/** Shows the dialogs. */
public void show() {
if (mIsSheet) {
mSheetDialog.show();
} else {
mManager.showDialog(mModalDialogModel, ModalDialogManager.ModalDialogType.APP);
}
}
/**
* Hides the dialog.
*
* @param animated Whether to animate the transition to hidden.
*/
public void dismiss(boolean animated) {
mDismissWithoutAnimation = !animated;
if (mIsSheet) {
mSheetDialog.dismiss();
} else {
mManager.dismissDialog(mModalDialogModel, DialogDismissalCause.UNKNOWN);
}
}
private Dialog createSheetDialog(Context context, View container) {
Dialog sheetDialog = new Dialog(context) {
private void superDismiss() {
super.dismiss();
}
@Override
public void dismiss() {
if (mDismissWithoutAnimation) {
// Dismiss the modal dialogs without any custom animations.
super.dismiss();
} else {
createAllAnimations(false, () -> {
// onAnimationEnd is called during the final frame of the animation.
// Delay the cleanup by a tiny amount to give this frame a chance to
// be displayed before we destroy the dialog.
mView.postDelayed(this ::superDismiss, CLOSE_CLEANUP_DELAY_MS);
}).start();
}
}
};
sheetDialog.requestWindowFeature(Window.FEATURE_NO_TITLE);
sheetDialog.setCanceledOnTouchOutside(true);
Window window = sheetDialog.getWindow();
window.setGravity(Gravity.TOP);
window.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
sheetDialog.setOnDismissListener(new DialogInterface.OnDismissListener() {
@Override
public void onDismiss(DialogInterface dialog) {
mController.onDismiss(null, DialogDismissalCause.UNKNOWN);
}
});
sheetDialog.addContentView(container,
new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.MATCH_PARENT));
// This must be called after addContentView, or it won't fully fill to the edge.
window.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
return sheetDialog;
}
private PropertyModel createModalDialog(View container) {
return new PropertyModel.Builder(ModalDialogProperties.ALL_KEYS)
.with(ModalDialogProperties.CONTROLLER, mController)
.with(ModalDialogProperties.CUSTOM_VIEW, container)
.with(ModalDialogProperties.CANCEL_ON_TOUCH_OUTSIDE, true)
.build();
}
private ViewGroup createSheetContainer(Context context, View tabView) {
return new ScrollView(context) {
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
heightMeasureSpec = MeasureSpec.makeMeasureSpec(
tabView != null ? tabView.getHeight() : 0, MeasureSpec.AT_MOST);
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
};
}
/**
* Create an animator to slide in the entire dialog from the top of the screen.
*/
private Animator createDialogSlideAnimaton(boolean isEnter) {
final float animHeight = -mView.getHeight();
ObjectAnimator translateAnim;
if (isEnter) {
mView.setTranslationY(animHeight);
translateAnim = ObjectAnimator.ofFloat(mView, View.TRANSLATION_Y, 0f);
translateAnim.setInterpolator(BakedBezierInterpolator.FADE_IN_CURVE);
} else {
translateAnim = ObjectAnimator.ofFloat(mView, View.TRANSLATION_Y, animHeight);
translateAnim.setInterpolator(BakedBezierInterpolator.FADE_OUT_CURVE);
}
translateAnim.setDuration(ENTER_EXIT_DURATION_MS);
return translateAnim;
}
/**
* Create an animator to show/hide the entire dialog. On phones the dialog is slid in as a
* sheet. Otherwise, the default fade-in is used.
*/
private Animator createAllAnimations(boolean isEnter, Runnable onAnimationEnd) {
Animator dialogAnimation =
mIsSheet ? createDialogSlideAnimaton(isEnter) : new AnimatorSet();
Animator viewAnimation = mView.createEnterExitAnimation(isEnter);
AnimatorSet allAnimations = new AnimatorSet();
if (isEnter) allAnimations.setStartDelay(ENTER_START_DELAY_MS);
allAnimations.playTogether(dialogAnimation, viewAnimation);
allAnimations.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
mCurrentAnimation = null;
if (onAnimationEnd == null) return;
onAnimationEnd.run();
}
});
if (mCurrentAnimation != null) mCurrentAnimation.cancel();
mCurrentAnimation = allAnimations;
return allAnimations;
}
}