1 // Copyright 2014 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 package org.chromium.chrome.browser.widget.accessibility;
7 import android.animation.Animator;
8 import android.animation.AnimatorListenerAdapter;
9 import android.animation.AnimatorSet;
10 import android.animation.ObjectAnimator;
11 import android.content.Context;
12 import android.graphics.Bitmap;
13 import android.os.Handler;
14 import android.util.AttributeSet;
15 import android.view.GestureDetector;
16 import android.view.MotionEvent;
17 import android.view.View;
18 import android.view.View.OnClickListener;
19 import android.view.ViewGroup;
20 import android.widget.AbsListView;
21 import android.widget.Button;
22 import android.widget.FrameLayout;
23 import android.widget.ImageButton;
24 import android.widget.ImageView;
25 import android.widget.LinearLayout;
26 import android.widget.TextView;
28 import org.chromium.base.VisibleForTesting;
29 import org.chromium.chrome.R;
30 import org.chromium.chrome.browser.EmptyTabObserver;
31 import org.chromium.chrome.browser.Tab;
32 import org.chromium.chrome.browser.TabObserver;
35 * A widget that shows a single row of the {@link AccessibilityTabModelListView} list.
36 * This list shows both the title of the {@link Tab} as well as a close button to close
39 public class AccessibilityTabModelListItem extends FrameLayout implements OnClickListener {
40 private static final int CLOSE_ANIMATION_DURATION_MS = 100;
41 private static final int DEFAULT_ANIMATION_DURATION_MS = 300;
42 private static final int VELOCITY_SCALING_FACTOR = 150;
43 private static final int CLOSE_TIMEOUT_MS = 2000;
45 private int mCloseAnimationDurationMs;
46 private int mDefaultAnimationDurationMs;
47 private int mCloseTimeoutMs;
48 // The last run animation (if non-null, it still might have already completed).
49 private Animator mActiveAnimation;
51 private final float mSwipeCommitDistance;
52 private final float mFlingCommitDistance;
54 // Keeps track of how a tab was closed
55 // < 0 : swiped to the left.
56 // > 0 : swiped to the right.
57 // = 0 : closed with the close button.
58 private float mSwipedAway;
60 // The children on the standard view.
61 private LinearLayout mTabContents;
62 private TextView mTitleView;
63 private ImageView mFaviconView;
64 private ImageButton mCloseButton;
66 // The children on the undo view.
67 private LinearLayout mUndoContents;
68 private Button mUndoButton;
71 private boolean mCanUndo;
72 private AccessibilityTabModelListItemListener mListener;
73 private final GestureDetector mSwipeGestureDetector;
74 private final int mDefaultHeight;
75 private AccessibilityTabModelListView mCanScrollListener;
78 * An interface that exposes actions taken on this item. The registered listener will be
79 * sent selection and close events based on user input.
81 public interface AccessibilityTabModelListItemListener {
83 * Called when a user clicks on this list item.
84 * @param tabId The ID of the tab that this list item represents.
86 public void tabSelected(int tabId);
89 * Called when a user clicks on the close button of this list item.
90 * @param tabId The ID of the tab that this list item represents.
92 public void tabClosed(int tabId);
95 * Called when the data corresponding to this list item has changed.
96 * @param tabId The ID of the tab that this list item represents.
98 public void tabChanged(int tabId);
101 * @return Whether or not the tab is scheduled to be closed.
103 public boolean hasPendingClosure(int tabId);
106 * Schedule a tab to be closed in the future.
107 * @param tabId The ID of the tab to close.
109 public void schedulePendingClosure(int tabId);
112 * Cancel a tab's closure.
113 * @param tabId The ID of the tab that should no longer be closed.
115 public void cancelPendingClosure(int tabId);
118 private final Runnable mCloseRunnable = new Runnable() {
125 private final Handler mHandler = new Handler();
128 * Used with the swipe away and blink out animations to bring in the undo view.
130 private final AnimatorListenerAdapter mCloseAnimatorListener =
131 new AnimatorListenerAdapter() {
132 private boolean mIsCancelled;
135 public void onAnimationStart(Animator animation) {
136 mIsCancelled = false;
140 public void onAnimationCancel(Animator animation) {
145 public void onAnimationEnd(Animator animator) {
146 if (mIsCancelled) return;
148 mListener.schedulePendingClosure(mTab.getId());
149 setTranslationX(0.f);
154 runResetAnimation(false);
155 mHandler.postDelayed(mCloseRunnable, mCloseTimeoutMs);
160 * Used with the close animation to actually close a tab after it has shrunk away.
162 private final AnimatorListenerAdapter mActuallyCloseAnimatorListener =
163 new AnimatorListenerAdapter() {
164 private boolean mIsCancelled;
167 public void onAnimationStart(Animator animation) {
168 mIsCancelled = false;
172 public void onAnimationCancel(Animator animation) {
177 public void onAnimationEnd(Animator animator) {
178 if (mIsCancelled) return;
182 mTabContents.setAlpha(1.f);
183 mUndoContents.setAlpha(1.f);
184 cancelRunningAnimation();
185 mListener.tabClosed(mTab.getId());
190 * @param context The Context to build this widget in.
191 * @param attrs The AttributeSet to use to build this widget.
193 public AccessibilityTabModelListItem(Context context, AttributeSet attrs) {
194 super(context, attrs);
195 mSwipeGestureDetector = new GestureDetector(context, new SwipeGestureListener());
196 mSwipeCommitDistance =
197 context.getResources().getDimension(R.dimen.swipe_commit_distance);
198 mFlingCommitDistance = mSwipeCommitDistance / 3;
201 context.getResources().getDimensionPixelOffset(R.dimen.accessibility_tab_height);
203 mCloseAnimationDurationMs = CLOSE_ANIMATION_DURATION_MS;
204 mDefaultAnimationDurationMs = DEFAULT_ANIMATION_DURATION_MS;
205 mCloseTimeoutMs = CLOSE_TIMEOUT_MS;
209 public void onFinishInflate() {
210 super.onFinishInflate();
211 mTabContents = (LinearLayout) findViewById(R.id.tab_contents);
212 mTitleView = (TextView) findViewById(R.id.tab_title);
213 mFaviconView = (ImageView) findViewById(R.id.tab_favicon);
214 mCloseButton = (ImageButton) findViewById(R.id.close_btn);
216 mUndoContents = (LinearLayout) findViewById(R.id.undo_contents);
217 mUndoButton = (Button) findViewById(R.id.undo_button);
222 mCloseButton.setOnClickListener(this);
223 mUndoButton.setOnClickListener(this);
224 setOnClickListener(this);
228 * Sets the {@link Tab} this {@link View} will represent in the list.
229 * @param tab The {@link Tab} to represent.
230 * @param canUndo Whether or not closing this {@link Tab} can be undone.
232 public void setTab(Tab tab, boolean canUndo) {
233 if (mTab != null) mTab.removeObserver(mTabObserver);
235 tab.addObserver(mTabObserver);
241 private void showUndoView(boolean showView) {
242 if (showView && mCanUndo) {
243 mUndoContents.setVisibility(View.VISIBLE);
244 mTabContents.setVisibility(View.INVISIBLE);
246 mTabContents.setVisibility(View.VISIBLE);
247 mUndoContents.setVisibility(View.INVISIBLE);
254 * Registers a listener to be notified of selection and close events taken on this list item.
255 * @param listener The listener to be notified of selection and close events.
257 public void setListeners(AccessibilityTabModelListItemListener listener,
258 AccessibilityTabModelListView canScrollListener) {
259 mListener = listener;
260 mCanScrollListener = canScrollListener;
263 private void updateTabTitle() {
264 String title = mTab != null ? mTab.getTitle() : null;
265 if (title == null || title.isEmpty()) {
266 title = getContext().getResources().getString(R.string.tab_loading_default_title);
269 if (!title.equals(mTitleView.getText())) mTitleView.setText(title);
271 String accessibilityString = getContext().getString(R.string.accessibility_tabstrip_tab,
273 if (!accessibilityString.equals(getContentDescription())) {
274 setContentDescription(getContext().getString(R.string.accessibility_tabstrip_tab,
279 private void updateFavicon() {
281 Bitmap bitmap = mTab.getFavicon();
282 if (bitmap != null) {
283 mFaviconView.setImageBitmap(bitmap);
285 mFaviconView.setImageResource(R.drawable.globe_incognito_favicon);
291 public void onClick(View v) {
292 if (mListener == null) return;
294 int tabId = mTab.getId();
295 if (v == AccessibilityTabModelListItem.this && !mListener.hasPendingClosure(tabId)) {
296 mListener.tabSelected(tabId);
297 } else if (v == mCloseButton) {
299 runBlinkOutAnimation();
303 } else if (v == mUndoButton) {
304 // Kill the close action.
305 mHandler.removeCallbacks(mCloseRunnable);
307 mListener.cancelPendingClosure(tabId);
310 if (mSwipedAway > 0.f) {
311 setTranslationX(getWidth());
312 runResetAnimation(false);
313 } else if (mSwipedAway < 0.f) {
314 setTranslationX(-getWidth());
315 runResetAnimation(false);
319 runResetAnimation(true);
325 protected void onDetachedFromWindow() {
326 super.onDetachedFromWindow();
327 if (mTab != null) mTab.removeObserver(mTabObserver);
328 cancelRunningAnimation();
331 private final TabObserver mTabObserver = new EmptyTabObserver() {
333 public void onFaviconUpdated(Tab tab) {
335 notifyTabUpdated(tab);
339 public void onTitleUpdated(Tab tab) {
341 notifyTabUpdated(tab);
345 public void onUrlUpdated(Tab tab) {
347 notifyTabUpdated(tab);
352 public boolean onTouchEvent(MotionEvent e) {
353 // If there is a pending close task, remove it.
354 mHandler.removeCallbacks(mCloseRunnable);
356 boolean handled = mSwipeGestureDetector.onTouchEvent(e);
357 if (handled) return true;
358 if (e.getActionMasked() == MotionEvent.ACTION_UP) {
359 if (Math.abs(getTranslationX()) > mSwipeCommitDistance) {
360 runSwipeAnimation(DEFAULT_ANIMATION_DURATION_MS);
362 runResetAnimation(false);
364 mCanScrollListener.setCanScroll(true);
367 return super.onTouchEvent(e);
371 * This call is exposed for the benefit of the animators.
373 * @param height The height of the current view.
375 public void setHeight(int height) {
376 AbsListView.LayoutParams params = (AbsListView.LayoutParams) getLayoutParams();
377 if (params == null) {
378 params = new AbsListView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, height);
380 if (params.height == height) return;
381 params.height = height;
383 setLayoutParams(params);
387 * Used to reset the state because views are recycled.
389 public void resetState() {
390 setTranslationX(0.f);
394 setHeight(mDefaultHeight);
395 cancelRunningAnimation();
396 // Remove any callbacks.
397 mHandler.removeCallbacks(mCloseRunnable);
399 if (mListener != null) {
400 boolean hasPendingClosure = mListener.hasPendingClosure(mTab.getId());
401 showUndoView(hasPendingClosure);
402 if (hasPendingClosure) mHandler.postDelayed(mCloseRunnable, mCloseTimeoutMs);
409 * Simple gesture listener to catch the scroll and fling gestures on the list item.
411 private class SwipeGestureListener extends GestureDetector.SimpleOnGestureListener {
413 public boolean onDown(MotionEvent e) {
414 // Returns true so that we can handle events that start with an onDown.
419 public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
420 // Don't scroll if we're waiting for user interaction.
421 if (mListener.hasPendingClosure(mTab.getId())) return false;
423 // Stop the ListView from scrolling vertically.
424 mCanScrollListener.setCanScroll(false);
426 float distance = e2.getX() - e1.getX();
427 setTranslationX(distance + getTranslationX());
428 setAlpha(1 - Math.abs(getTranslationX() / getWidth()));
433 public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
434 // Arbitrary threshold that feels right.
435 if (Math.abs(getTranslationX()) < mFlingCommitDistance) return false;
437 double velocityMagnitude = Math.sqrt(velocityX * velocityX + velocityY * velocityY);
438 long closeTime = (long) Math.abs((getWidth() / velocityMagnitude)) *
439 VELOCITY_SCALING_FACTOR;
440 runSwipeAnimation(Math.min(closeTime, mDefaultAnimationDurationMs));
441 mCanScrollListener.setCanScroll(true);
446 public boolean onSingleTapConfirmed(MotionEvent e) {
453 public void disableAnimations() {
454 mCloseAnimationDurationMs = 0;
455 mDefaultAnimationDurationMs = 0;
460 public boolean hasPendingClosure() {
461 if (mListener != null) return mListener.hasPendingClosure(mTab.getId());
465 private void runSwipeAnimation(long time) {
466 cancelRunningAnimation();
467 mSwipedAway = getTranslationX();
469 ObjectAnimator swipe = ObjectAnimator.ofFloat(this, View.TRANSLATION_X,
470 getTranslationX() > 0 ? getWidth() : -getWidth());
471 ObjectAnimator fadeOut = ObjectAnimator.ofFloat(this, View.ALPHA, 0.f);
473 AnimatorSet set = new AnimatorSet();
474 set.playTogether(fadeOut, swipe);
475 set.addListener(mCloseAnimatorListener);
476 set.setDuration(Math.min(time, mDefaultAnimationDurationMs));
479 mActiveAnimation = set;
482 private void runResetAnimation(boolean useCloseAnimationDuration) {
483 cancelRunningAnimation();
485 ObjectAnimator swipe = ObjectAnimator.ofFloat(this, View.TRANSLATION_X, 0.f);
486 ObjectAnimator fadeIn = ObjectAnimator.ofFloat(this, View.ALPHA, 1.f);
487 ObjectAnimator scaleX = ObjectAnimator.ofFloat(this, View.SCALE_X, 1.f);
488 ObjectAnimator scaleY = ObjectAnimator.ofFloat(this, View.SCALE_Y, 1.f);
489 ObjectAnimator resetHeight = ObjectAnimator.ofInt(this, "height", mDefaultHeight);
491 AnimatorSet set = new AnimatorSet();
492 set.playTogether(swipe, fadeIn, scaleX, scaleY, resetHeight);
493 set.setDuration(useCloseAnimationDuration
494 ? mCloseAnimationDurationMs : mDefaultAnimationDurationMs);
497 mActiveAnimation = set;
500 private void runBlinkOutAnimation() {
501 cancelRunningAnimation();
504 ObjectAnimator stretchX = ObjectAnimator.ofFloat(this, View.SCALE_X, 1.2f);
505 ObjectAnimator shrinkY = ObjectAnimator.ofFloat(this, View.SCALE_Y, 0.f);
506 ObjectAnimator fadeOut = ObjectAnimator.ofFloat(this, View.ALPHA, 0.f);
508 AnimatorSet set = new AnimatorSet();
509 set.playTogether(fadeOut, shrinkY, stretchX);
510 set.addListener(mCloseAnimatorListener);
511 set.setDuration(mCloseAnimationDurationMs);
514 mActiveAnimation = set;
517 private void runCloseAnimation() {
518 cancelRunningAnimation();
520 ObjectAnimator shrinkHeight = ObjectAnimator.ofInt(this, "height", 0);
521 ObjectAnimator shrinkY = ObjectAnimator.ofFloat(this, View.SCALE_Y, 0.f);
523 AnimatorSet set = new AnimatorSet();
524 set.playTogether(shrinkHeight, shrinkY);
525 set.addListener(mActuallyCloseAnimatorListener);
526 set.setDuration(mDefaultAnimationDurationMs);
529 mActiveAnimation = set;
532 private void cancelRunningAnimation() {
533 if (mActiveAnimation != null && mActiveAnimation.isRunning()) mActiveAnimation.cancel();
535 mActiveAnimation = null;
538 private void notifyTabUpdated(Tab tab) {
539 if (mListener != null) mListener.tabChanged(tab.getId());