blob: e4895b7a8fa27b054613ff81f4bcf1e3efdcf96d [file] [log] [blame]
// Copyright 2019 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.tasks.tab_management;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.animation.ValueAnimator;
import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Rect;
import android.os.SystemClock;
import android.support.annotation.Nullable;
import android.support.v7.widget.RecyclerView;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewParent;
import org.chromium.chrome.browser.ChromeFeatureList;
import org.chromium.ui.interpolators.BakedBezierInterpolator;
import org.chromium.ui.resources.dynamics.DynamicResourceLoader;
import org.chromium.ui.resources.dynamics.ViewResourceAdapter;
/**
* A custom RecyclerView implementation for the tab grid, to handle show/hide logic in class.
*/
class TabListRecyclerView extends RecyclerView {
public static final long BASE_ANIMATION_DURATION_MS = 218;
public static final long FINAL_FADE_IN_DURATION_MS = 50;
public static final long RESTORE_ANIMATION_DURATION_MS = 10;
public static final int ANIMATION_STATUS_RESTORE = 0;
public static final int ANIMATION_STATUS_ZOOM_OUT = 1;
public static final int ANIMATION_STATUS_ZOOM_IN = 2;
/**
* Field trial parameter for downsampling scaling factor.
*/
private static final String DOWNSAMPLING_SCALE_PARAM = "downsampling-scale";
private static final float DEFAULT_DOWNSAMPLING_SCALE = 0.5f;
/**
* An interface to listen to visibility related changes on this {@link RecyclerView}.
*/
interface VisibilityListener {
/**
* Called before the animation to show the tab list has started.
* @param isAnimating Whether visibility is changing with animation
*/
void startedShowing(boolean isAnimating);
/**
* Called when the animation to show the tab list is finished.
*/
void finishedShowing();
/**
* Called before the animation to hide the tab list has started.
* @param isAnimating Whether visibility is changing with animation
*/
void startedHiding(boolean isAnimating);
/**
* Called when the animation to show the tab list is finished.
*/
void finishedHiding();
}
private ValueAnimator mFadeInAnimator;
private ValueAnimator mFadeOutAnimator;
private VisibilityListener mListener;
private ViewResourceAdapter mDynamicView;
private long mLastDirtyTime;
private RecyclerView.ItemAnimator mOriginalAnimator;
/**
* Basic constructor to use during inflation from xml.
*/
public TabListRecyclerView(Context context, AttributeSet attributeSet) {
super(context, attributeSet);
}
/**
* Set the {@link VisibilityListener} that will listen on granular visibility events.
* @param listener The {@link VisibilityListener} to use.
*/
void setVisibilityListener(VisibilityListener listener) {
mListener = listener;
}
void prepareOverview() {
endAllAnimations();
// Stop all the animations to make all the items show up and scroll to position immediately.
mOriginalAnimator = getItemAnimator();
setItemAnimator(null);
}
/**
* Start showing the tab list.
* @param animate Whether the visibility change should be animated.
*/
void startShowing(boolean animate) {
assert mFadeOutAnimator == null;
mListener.startedShowing(animate);
long duration = ChromeFeatureList.isEnabled(ChromeFeatureList.TAB_TO_GTS_ANIMATION)
? FINAL_FADE_IN_DURATION_MS
: BASE_ANIMATION_DURATION_MS;
setAlpha(0);
setVisibility(View.VISIBLE);
mFadeInAnimator = ObjectAnimator.ofFloat(this, View.ALPHA, 1);
mFadeInAnimator.setInterpolator(BakedBezierInterpolator.FADE_IN_CURVE);
mFadeInAnimator.setDuration(duration);
mFadeInAnimator.start();
mFadeInAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
mFadeInAnimator = null;
mListener.finishedShowing();
// Restore the original value.
setItemAnimator(mOriginalAnimator);
if (mDynamicView != null)
mDynamicView.dropCachedBitmap();
}
});
if (!animate) mFadeInAnimator.end();
}
/**
* @return The ID for registering and using the dynamic resource in compositor.
*/
int getResourceId() {
return getId();
}
long getLastDirtyTimeForTesting() {
return mLastDirtyTime;
}
private float getDownsamplingScale() {
String scale = ChromeFeatureList.getFieldTrialParamByFeature(
ChromeFeatureList.TAB_TO_GTS_ANIMATION, DOWNSAMPLING_SCALE_PARAM);
try {
return Float.valueOf(scale);
} catch (NumberFormatException e) {
return DEFAULT_DOWNSAMPLING_SCALE;
}
}
/**
* Create a DynamicResource for this RecyclerView.
* The view resource can be obtained by {@link #getResourceId} in compositor layer.
*/
void createDynamicView(DynamicResourceLoader loader) {
mDynamicView = new ViewResourceAdapter(this) {
@Override
public boolean isDirty() {
boolean dirty = super.isDirty();
if (dirty) mLastDirtyTime = SystemClock.elapsedRealtime();
return dirty;
}
};
mDynamicView.setDownsamplingScale(getDownsamplingScale());
loader.registerResource(getResourceId(), mDynamicView);
}
@SuppressLint("NewApi") // Used on O+, invalidateChildInParent used for previous versions.
@Override
public void onDescendantInvalidated(View child, View target) {
super.onDescendantInvalidated(child, target);
if (mDynamicView != null) {
mDynamicView.invalidate(null);
}
}
@Override
public ViewParent invalidateChildInParent(int[] location, Rect dirty) {
ViewParent retVal = super.invalidateChildInParent(location, dirty);
if (mDynamicView != null) {
mDynamicView.invalidate(dirty);
}
return retVal;
}
/**
* Start hiding the tab list.
* @param animate Whether the visibility change should be animated.
*/
void startHiding(boolean animate) {
endAllAnimations();
mListener.startedHiding(animate);
mFadeOutAnimator = ObjectAnimator.ofFloat(this, View.ALPHA, 0);
mFadeOutAnimator.setInterpolator(BakedBezierInterpolator.FADE_OUT_CURVE);
mFadeOutAnimator.setDuration(BASE_ANIMATION_DURATION_MS);
mFadeOutAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
mFadeOutAnimator = null;
setVisibility(View.INVISIBLE);
mListener.finishedHiding();
}
});
mFadeOutAnimator.start();
if (!animate) mFadeOutAnimator.end();
}
void postHiding() {
if (mDynamicView != null) {
mDynamicView.dropCachedBitmap();
}
}
private void endAllAnimations() {
if (mFadeInAnimator != null) {
mFadeInAnimator.end();
}
if (mFadeOutAnimator != null) {
mFadeOutAnimator.end();
}
}
/**
* @param currentTabIndex The the current tab's index in the model.
* @return The {@link Rect} of the thumbnail of the current tab, relative to the
* {@link TabListRecyclerView} coordinates.
*/
@Nullable
Rect getRectOfCurrentThumbnail(int currentTabIndex) {
TabGridViewHolder holder =
(TabGridViewHolder) findViewHolderForAdapterPosition(currentTabIndex);
if (holder == null) return null;
int[] loc = new int[2];
holder.thumbnail.getLocationInWindow(loc);
Rect rect = new Rect(loc[0], loc[1], loc[0] + holder.thumbnail.getWidth(),
loc[1] + holder.thumbnail.getHeight());
getLocationInWindow(loc);
rect.top -= loc[1];
rect.bottom -= loc[1];
return rect;
}
static void scaleTabGridCardView(View view, int status) {
AnimatorSet scaleAnimator = new AnimatorSet();
float scale = status == ANIMATION_STATUS_ZOOM_IN ? 0.8f : 1f;
ObjectAnimator scaleX = ObjectAnimator.ofFloat(view, "scaleX", scale);
ObjectAnimator scaleY = ObjectAnimator.ofFloat(view, "scaleY", scale);
scaleX.setDuration(status == ANIMATION_STATUS_RESTORE ? RESTORE_ANIMATION_DURATION_MS
: BASE_ANIMATION_DURATION_MS);
scaleY.setDuration(status == ANIMATION_STATUS_RESTORE ? RESTORE_ANIMATION_DURATION_MS
: BASE_ANIMATION_DURATION_MS);
scaleAnimator.play(scaleX).with(scaleY);
scaleAnimator.start();
}
/**
* This method finds out the index of the hovered tab's viewHolder in {@code recyclerView}.
* @param recyclerView The recyclerview that owns the tabs' viewHolders.
* @param view The view of the selected tab.
* @param dX The X offset of the selected tab.
* @param dY The Y offset of the selected tab.
* @param threshold The threshold to judge whether two tabs are overlapped.
* @return The index of the hovered tab.
*/
static int getHoveredTabIndex(
RecyclerView recyclerView, View view, float dX, float dY, float threshold) {
for (int i = 0; i < recyclerView.getChildCount(); i++) {
View child = recyclerView.getChildAt(i);
if (child.getLeft() == view.getLeft() && child.getTop() == view.getTop()) {
continue;
}
if (isOverlap(child.getLeft(), child.getTop(), view.getLeft() + dX, view.getTop() + dY,
threshold)) {
return i;
}
}
return -1;
}
private static boolean isOverlap(
float left1, float top1, float left2, float top2, float threshold) {
return Math.abs(left1 - left2) < threshold && Math.abs(top1 - top2) < threshold;
}
}