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()); |
+ } |
+} |