| Index: chrome/android/java/src/org/chromium/chrome/browser/widget/accessibility/AccessibilityTabModelListItem.java
|
| diff --git a/chrome/android/java/src/org/chromium/chrome/browser/widget/accessibility/AccessibilityTabModelListItem.java b/chrome/android/java/src/org/chromium/chrome/browser/widget/accessibility/AccessibilityTabModelListItem.java
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..08f1a71de0bd14a7055035c20a47f968d98054f4
|
| --- /dev/null
|
| +++ b/chrome/android/java/src/org/chromium/chrome/browser/widget/accessibility/AccessibilityTabModelListItem.java
|
| @@ -0,0 +1,543 @@
|
| +// Copyright 2014 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.widget.accessibility;
|
| +
|
| +import android.animation.Animator;
|
| +import android.animation.AnimatorListenerAdapter;
|
| +import android.animation.AnimatorSet;
|
| +import android.animation.ObjectAnimator;
|
| +import android.content.Context;
|
| +import android.graphics.Bitmap;
|
| +import android.os.Handler;
|
| +import android.util.AttributeSet;
|
| +import android.view.GestureDetector;
|
| +import android.view.MotionEvent;
|
| +import android.view.View;
|
| +import android.view.View.OnClickListener;
|
| +import android.view.ViewGroup;
|
| +import android.widget.AbsListView;
|
| +import android.widget.Button;
|
| +import android.widget.FrameLayout;
|
| +import android.widget.ImageButton;
|
| +import android.widget.ImageView;
|
| +import android.widget.LinearLayout;
|
| +import android.widget.TextView;
|
| +
|
| +import com.google.common.annotations.VisibleForTesting;
|
| +import com.google.common.base.Strings;
|
| +
|
| +import org.chromium.chrome.R;
|
| +import org.chromium.chrome.browser.EmptyTabObserver;
|
| +import org.chromium.chrome.browser.Tab;
|
| +import org.chromium.chrome.browser.TabObserver;
|
| +
|
| +/**
|
| + * A widget that shows a single row of the {@link AccessibilityTabModelListView} list.
|
| + * This list shows both the title of the {@link Tab} as well as a close button to close
|
| + * the tab.
|
| + */
|
| +public class AccessibilityTabModelListItem extends FrameLayout implements OnClickListener {
|
| + private static final int CLOSE_ANIMATION_DURATION_MS = 100;
|
| + private static final int DEFAULT_ANIMATION_DURATION_MS = 300;
|
| + private static final int VELOCITY_SCALING_FACTOR = 150;
|
| + private static final int CLOSE_TIMEOUT_MS = 2000;
|
| +
|
| + private int mCloseAnimationDurationMs;
|
| + private int mDefaultAnimationDurationMs;
|
| + private int mCloseTimeoutMs;
|
| + // The last run animation (if non-null, it still might have already completed).
|
| + private Animator mActiveAnimation;
|
| +
|
| + private final float mSwipeCommitDistance;
|
| + private final float mFlingCommitDistance;
|
| +
|
| + // Keeps track of how a tab was closed
|
| + // < 0 : swiped to the left.
|
| + // > 0 : swiped to the right.
|
| + // = 0 : closed with the close button.
|
| + private float mSwipedAway;
|
| +
|
| + // The children on the standard view.
|
| + private LinearLayout mTabContents;
|
| + private TextView mTitleView;
|
| + private ImageView mFaviconView;
|
| + private ImageButton mCloseButton;
|
| +
|
| + // The children on the undo view.
|
| + private LinearLayout mUndoContents;
|
| + private Button mUndoButton;
|
| +
|
| + private Tab mTab;
|
| + private boolean mCanUndo;
|
| + private AccessibilityTabModelListItemListener mListener;
|
| + private final GestureDetector mSwipeGestureDetector;
|
| + private final int mDefaultHeight;
|
| + private AccessibilityTabModelListView mCanScrollListener;
|
| +
|
| + /**
|
| + * An interface that exposes actions taken on this item. The registered listener will be
|
| + * sent selection and close events based on user input.
|
| + */
|
| + public interface AccessibilityTabModelListItemListener {
|
| + /**
|
| + * Called when a user clicks on this list item.
|
| + * @param tabId The ID of the tab that this list item represents.
|
| + */
|
| + public void tabSelected(int tabId);
|
| +
|
| + /**
|
| + * Called when a user clicks on the close button of this list item.
|
| + * @param tabId The ID of the tab that this list item represents.
|
| + */
|
| + public void tabClosed(int tabId);
|
| +
|
| + /**
|
| + * Called when the data corresponding to this list item has changed.
|
| + * @param tabId The ID of the tab that this list item represents.
|
| + */
|
| + public void tabChanged(int tabId);
|
| +
|
| + /**
|
| + * @return Whether or not the tab is scheduled to be closed.
|
| + */
|
| + public boolean hasPendingClosure(int tabId);
|
| +
|
| + /**
|
| + * Schedule a tab to be closed in the future.
|
| + * @param tabId The ID of the tab to close.
|
| + */
|
| + public void schedulePendingClosure(int tabId);
|
| +
|
| + /**
|
| + * Cancel a tab's closure.
|
| + * @param tabId The ID of the tab that should no longer be closed.
|
| + */
|
| + public void cancelPendingClosure(int tabId);
|
| + }
|
| +
|
| + private final Runnable mCloseRunnable = new Runnable() {
|
| + @Override
|
| + public void run() {
|
| + runCloseAnimation();
|
| + }
|
| + };
|
| +
|
| + private final Handler mHandler = new Handler();
|
| +
|
| + /**
|
| + * Used with the swipe away and blink out animations to bring in the undo view.
|
| + */
|
| + private final AnimatorListenerAdapter mCloseAnimatorListener =
|
| + new AnimatorListenerAdapter() {
|
| + private boolean mIsCancelled;
|
| +
|
| + @Override
|
| + public void onAnimationStart(Animator animation) {
|
| + mIsCancelled = false;
|
| + }
|
| +
|
| + @Override
|
| + public void onAnimationCancel(Animator animation) {
|
| + mIsCancelled = true;
|
| + }
|
| +
|
| + @Override
|
| + public void onAnimationEnd(Animator animator) {
|
| + if (mIsCancelled) return;
|
| +
|
| + mListener.schedulePendingClosure(mTab.getId());
|
| + setTranslationX(0.f);
|
| + setScaleX(1.f);
|
| + setScaleY(1.f);
|
| + setAlpha(0.f);
|
| + showUndoView(true);
|
| + runResetAnimation(false);
|
| + mHandler.postDelayed(mCloseRunnable, mCloseTimeoutMs);
|
| + }
|
| + };
|
| +
|
| + /**
|
| + * Used with the close animation to actually close a tab after it has shrunk away.
|
| + */
|
| + private final AnimatorListenerAdapter mActuallyCloseAnimatorListener =
|
| + new AnimatorListenerAdapter() {
|
| + private boolean mIsCancelled;
|
| +
|
| + @Override
|
| + public void onAnimationStart(Animator animation) {
|
| + mIsCancelled = false;
|
| + }
|
| +
|
| + @Override
|
| + public void onAnimationCancel(Animator animation) {
|
| + mIsCancelled = true;
|
| + }
|
| +
|
| + @Override
|
| + public void onAnimationEnd(Animator animator) {
|
| + if (mIsCancelled) return;
|
| +
|
| + showUndoView(false);
|
| + setAlpha(1.f);
|
| + mTabContents.setAlpha(1.f);
|
| + mUndoContents.setAlpha(1.f);
|
| + cancelRunningAnimation();
|
| + mListener.tabClosed(mTab.getId());
|
| + }
|
| + };
|
| +
|
| + /**
|
| + * @param context The Context to build this widget in.
|
| + * @param attrs The AttributeSet to use to build this widget.
|
| + */
|
| + public AccessibilityTabModelListItem(Context context, AttributeSet attrs) {
|
| + super(context, attrs);
|
| + mSwipeGestureDetector = new GestureDetector(context, new SwipeGestureListener());
|
| + mSwipeCommitDistance =
|
| + context.getResources().getDimension(R.dimen.swipe_commit_distance);
|
| + mFlingCommitDistance = mSwipeCommitDistance / 3;
|
| +
|
| + mDefaultHeight =
|
| + context.getResources().getDimensionPixelOffset(R.dimen.accessibility_tab_height);
|
| +
|
| + mCloseAnimationDurationMs = CLOSE_ANIMATION_DURATION_MS;
|
| + mDefaultAnimationDurationMs = DEFAULT_ANIMATION_DURATION_MS;
|
| + mCloseTimeoutMs = CLOSE_TIMEOUT_MS;
|
| + }
|
| +
|
| + @Override
|
| + public void onFinishInflate() {
|
| + super.onFinishInflate();
|
| + mTabContents = (LinearLayout) findViewById(R.id.tab_contents);
|
| + mTitleView = (TextView) findViewById(R.id.tab_title);
|
| + mFaviconView = (ImageView) findViewById(R.id.tab_favicon);
|
| + mCloseButton = (ImageButton) findViewById(R.id.close_btn);
|
| +
|
| + mUndoContents = (LinearLayout) findViewById(R.id.undo_contents);
|
| + mUndoButton = (Button) findViewById(R.id.undo_button);
|
| +
|
| + setClickable(true);
|
| + setFocusable(true);
|
| +
|
| + mCloseButton.setOnClickListener(this);
|
| + mUndoButton.setOnClickListener(this);
|
| + setOnClickListener(this);
|
| + }
|
| +
|
| + /**
|
| + * Sets the {@link Tab} this {@link View} will represent in the list.
|
| + * @param tab The {@link Tab} to represent.
|
| + * @param canUndo Whether or not closing this {@link Tab} can be undone.
|
| + */
|
| + public void setTab(Tab tab, boolean canUndo) {
|
| + if (mTab != null) mTab.removeObserver(mTabObserver);
|
| + mTab = tab;
|
| + tab.addObserver(mTabObserver);
|
| + mCanUndo = canUndo;
|
| + updateTabTitle();
|
| + updateFavicon();
|
| + }
|
| +
|
| + private void showUndoView(boolean showView) {
|
| + if (showView && mCanUndo) {
|
| + mUndoContents.setVisibility(View.VISIBLE);
|
| + mTabContents.setVisibility(View.INVISIBLE);
|
| + } else {
|
| + mTabContents.setVisibility(View.VISIBLE);
|
| + mUndoContents.setVisibility(View.INVISIBLE);
|
| + updateTabTitle();
|
| + updateFavicon();
|
| + }
|
| + }
|
| +
|
| + /**
|
| + * Registers a listener to be notified of selection and close events taken on this list item.
|
| + * @param listener The listener to be notified of selection and close events.
|
| + */
|
| + public void setListeners(AccessibilityTabModelListItemListener listener,
|
| + AccessibilityTabModelListView canScrollListener) {
|
| + mListener = listener;
|
| + mCanScrollListener = canScrollListener;
|
| + }
|
| +
|
| + private void updateTabTitle() {
|
| + String title = mTab != null ? mTab.getTitle() : null;
|
| + if (Strings.isNullOrEmpty(title)) {
|
| + title = getContext().getResources().getString(R.string.tab_loading_default_title);
|
| + }
|
| +
|
| + if (!title.equals(mTitleView.getText())) mTitleView.setText(title);
|
| +
|
| + String accessibilityString = getContext().getString(R.string.accessibility_tabstrip_tab,
|
| + title);
|
| + if (!accessibilityString.equals(getContentDescription())) {
|
| + setContentDescription(getContext().getString(R.string.accessibility_tabstrip_tab,
|
| + title));
|
| + }
|
| + }
|
| +
|
| + private void updateFavicon() {
|
| + if (mTab != null) {
|
| + Bitmap bitmap = mTab.getFavicon();
|
| + if (bitmap != null) {
|
| + mFaviconView.setImageBitmap(bitmap);
|
| + } else {
|
| + mFaviconView.setImageResource(R.drawable.globe_incognito_favicon);
|
| + }
|
| + }
|
| + }
|
| +
|
| + @Override
|
| + public void onClick(View v) {
|
| + if (mListener == null) return;
|
| +
|
| + int tabId = mTab.getId();
|
| + if (v == AccessibilityTabModelListItem.this && !mListener.hasPendingClosure(tabId)) {
|
| + mListener.tabSelected(tabId);
|
| + } else if (v == mCloseButton) {
|
| + if (mCanUndo) {
|
| + runBlinkOutAnimation();
|
| + } else {
|
| + runCloseAnimation();
|
| + }
|
| + } else if (v == mUndoButton) {
|
| + // Kill the close action.
|
| + mHandler.removeCallbacks(mCloseRunnable);
|
| +
|
| + mListener.cancelPendingClosure(tabId);
|
| + showUndoView(false);
|
| + setAlpha(0.f);
|
| + if (mSwipedAway > 0.f) {
|
| + setTranslationX(getWidth());
|
| + runResetAnimation(false);
|
| + } else if (mSwipedAway < 0.f) {
|
| + setTranslationX(-getWidth());
|
| + runResetAnimation(false);
|
| + } else {
|
| + setScaleX(1.2f);
|
| + setScaleY(0.f);
|
| + runResetAnimation(true);
|
| + }
|
| + }
|
| + }
|
| +
|
| + @Override
|
| + protected void onDetachedFromWindow() {
|
| + super.onDetachedFromWindow();
|
| + if (mTab != null) mTab.removeObserver(mTabObserver);
|
| + cancelRunningAnimation();
|
| + }
|
| +
|
| + private final TabObserver mTabObserver = new EmptyTabObserver() {
|
| + @Override
|
| + public void onFaviconUpdated(Tab tab) {
|
| + updateFavicon();
|
| + notifyTabUpdated(tab);
|
| + }
|
| +
|
| + @Override
|
| + public void onTitleUpdated(Tab tab) {
|
| + updateTabTitle();
|
| + notifyTabUpdated(tab);
|
| + }
|
| +
|
| + @Override
|
| + public void onUrlUpdated(Tab tab) {
|
| + updateTabTitle();
|
| + notifyTabUpdated(tab);
|
| + }
|
| + };
|
| +
|
| + @Override
|
| + public boolean onTouchEvent(MotionEvent e) {
|
| + // If there is a pending close task, remove it.
|
| + mHandler.removeCallbacks(mCloseRunnable);
|
| +
|
| + boolean handled = mSwipeGestureDetector.onTouchEvent(e);
|
| + if (handled) return true;
|
| + if (e.getActionMasked() == MotionEvent.ACTION_UP) {
|
| + if (Math.abs(getTranslationX()) > mSwipeCommitDistance) {
|
| + runSwipeAnimation(DEFAULT_ANIMATION_DURATION_MS);
|
| + } else {
|
| + runResetAnimation(false);
|
| + }
|
| + mCanScrollListener.setCanScroll(true);
|
| + return true;
|
| + }
|
| + return super.onTouchEvent(e);
|
| + }
|
| +
|
| + /**
|
| + * This call is exposed for the benefit of the animators.
|
| + *
|
| + * @param height The height of the current view.
|
| + */
|
| + public void setHeight(int height) {
|
| + AbsListView.LayoutParams params = (AbsListView.LayoutParams) getLayoutParams();
|
| + if (params == null) {
|
| + params = new AbsListView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, height);
|
| + } else {
|
| + if (params.height == height) return;
|
| + params.height = height;
|
| + }
|
| + setLayoutParams(params);
|
| + }
|
| +
|
| + /**
|
| + * Used to reset the state because views are recycled.
|
| + */
|
| + public void resetState() {
|
| + setTranslationX(0.f);
|
| + setAlpha(1.f);
|
| + setScaleX(1.f);
|
| + setScaleY(1.f);
|
| + setHeight(mDefaultHeight);
|
| + cancelRunningAnimation();
|
| + // Remove any callbacks.
|
| + mHandler.removeCallbacks(mCloseRunnable);
|
| +
|
| + if (mListener != null) {
|
| + boolean hasPendingClosure = mListener.hasPendingClosure(mTab.getId());
|
| + showUndoView(hasPendingClosure);
|
| + if (hasPendingClosure) mHandler.postDelayed(mCloseRunnable, mCloseTimeoutMs);
|
| + } else {
|
| + showUndoView(false);
|
| + }
|
| + }
|
| +
|
| + /**
|
| + * Simple gesture listener to catch the scroll and fling gestures on the list item.
|
| + */
|
| + private class SwipeGestureListener extends GestureDetector.SimpleOnGestureListener {
|
| + @Override
|
| + public boolean onDown(MotionEvent e) {
|
| + // Returns true so that we can handle events that start with an onDown.
|
| + return true;
|
| + }
|
| +
|
| + @Override
|
| + public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
|
| + // Don't scroll if we're waiting for user interaction.
|
| + if (mListener.hasPendingClosure(mTab.getId())) return false;
|
| +
|
| + // Stop the ListView from scrolling vertically.
|
| + mCanScrollListener.setCanScroll(false);
|
| +
|
| + float distance = e2.getX() - e1.getX();
|
| + setTranslationX(distance + getTranslationX());
|
| + setAlpha(1 - Math.abs(getTranslationX() / getWidth()));
|
| + return true;
|
| + }
|
| +
|
| + @Override
|
| + public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
|
| + // Arbitrary threshold that feels right.
|
| + if (Math.abs(getTranslationX()) < mFlingCommitDistance) return false;
|
| +
|
| + double velocityMagnitude = Math.sqrt(velocityX * velocityX + velocityY * velocityY);
|
| + long closeTime = (long) Math.abs((getWidth() / velocityMagnitude)) *
|
| + VELOCITY_SCALING_FACTOR;
|
| + runSwipeAnimation(Math.min(closeTime, mDefaultAnimationDurationMs));
|
| + mCanScrollListener.setCanScroll(true);
|
| + return true;
|
| + }
|
| +
|
| + @Override
|
| + public boolean onSingleTapConfirmed(MotionEvent e) {
|
| + performClick();
|
| + return true;
|
| + }
|
| + }
|
| +
|
| + @VisibleForTesting
|
| + public void disableAnimations() {
|
| + mCloseAnimationDurationMs = 0;
|
| + mDefaultAnimationDurationMs = 0;
|
| + mCloseTimeoutMs = 0;
|
| + }
|
| +
|
| + @VisibleForTesting
|
| + public boolean hasPendingClosure() {
|
| + if (mListener != null) return mListener.hasPendingClosure(mTab.getId());
|
| + return false;
|
| + }
|
| +
|
| + private void runSwipeAnimation(long time) {
|
| + cancelRunningAnimation();
|
| + mSwipedAway = getTranslationX();
|
| +
|
| + ObjectAnimator swipe = ObjectAnimator.ofFloat(this, View.TRANSLATION_X,
|
| + getTranslationX() > 0 ? getWidth() : -getWidth());
|
| + ObjectAnimator fadeOut = ObjectAnimator.ofFloat(this, View.ALPHA, 0.f);
|
| +
|
| + AnimatorSet set = new AnimatorSet();
|
| + set.playTogether(fadeOut, swipe);
|
| + set.addListener(mCloseAnimatorListener);
|
| + set.setDuration(Math.min(time, mDefaultAnimationDurationMs));
|
| + set.start();
|
| +
|
| + mActiveAnimation = set;
|
| + }
|
| +
|
| + private void runResetAnimation(boolean useCloseAnimationDuration) {
|
| + cancelRunningAnimation();
|
| +
|
| + ObjectAnimator swipe = ObjectAnimator.ofFloat(this, View.TRANSLATION_X, 0.f);
|
| + ObjectAnimator fadeIn = ObjectAnimator.ofFloat(this, View.ALPHA, 1.f);
|
| + ObjectAnimator scaleX = ObjectAnimator.ofFloat(this, View.SCALE_X, 1.f);
|
| + ObjectAnimator scaleY = ObjectAnimator.ofFloat(this, View.SCALE_Y, 1.f);
|
| + ObjectAnimator resetHeight = ObjectAnimator.ofInt(this, "height", mDefaultHeight);
|
| +
|
| + AnimatorSet set = new AnimatorSet();
|
| + set.playTogether(swipe, fadeIn, scaleX, scaleY, resetHeight);
|
| + set.setDuration(useCloseAnimationDuration
|
| + ? mCloseAnimationDurationMs : mDefaultAnimationDurationMs);
|
| + set.start();
|
| +
|
| + mActiveAnimation = set;
|
| + }
|
| +
|
| + private void runBlinkOutAnimation() {
|
| + cancelRunningAnimation();
|
| + mSwipedAway = 0;
|
| +
|
| + ObjectAnimator stretchX = ObjectAnimator.ofFloat(this, View.SCALE_X, 1.2f);
|
| + ObjectAnimator shrinkY = ObjectAnimator.ofFloat(this, View.SCALE_Y, 0.f);
|
| + ObjectAnimator fadeOut = ObjectAnimator.ofFloat(this, View.ALPHA, 0.f);
|
| +
|
| + AnimatorSet set = new AnimatorSet();
|
| + set.playTogether(fadeOut, shrinkY, stretchX);
|
| + set.addListener(mCloseAnimatorListener);
|
| + set.setDuration(mCloseAnimationDurationMs);
|
| + set.start();
|
| +
|
| + mActiveAnimation = set;
|
| + }
|
| +
|
| + private void runCloseAnimation() {
|
| + cancelRunningAnimation();
|
| +
|
| + ObjectAnimator shrinkHeight = ObjectAnimator.ofInt(this, "height", 0);
|
| + ObjectAnimator shrinkY = ObjectAnimator.ofFloat(this, View.SCALE_Y, 0.f);
|
| +
|
| + AnimatorSet set = new AnimatorSet();
|
| + set.playTogether(shrinkHeight, shrinkY);
|
| + set.addListener(mActuallyCloseAnimatorListener);
|
| + set.setDuration(mDefaultAnimationDurationMs);
|
| + set.start();
|
| +
|
| + mActiveAnimation = set;
|
| + }
|
| +
|
| + private void cancelRunningAnimation() {
|
| + if (mActiveAnimation != null && mActiveAnimation.isRunning()) mActiveAnimation.cancel();
|
| +
|
| + mActiveAnimation = null;
|
| + }
|
| +
|
| + private void notifyTabUpdated(Tab tab) {
|
| + if (mListener != null) mListener.tabChanged(tab.getId());
|
| + }
|
| +}
|
|
|