Upstream version 10.39.225.0
[platform/framework/web/crosswalk.git] / src / chrome / android / java / src / org / chromium / chrome / browser / widget / accessibility / AccessibilityTabModelListItem.java
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.
4
5 package org.chromium.chrome.browser.widget.accessibility;
6
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;
27
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;
33
34 /**
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
37  * the tab.
38  */
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;
44
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;
50
51     private final float mSwipeCommitDistance;
52     private final float mFlingCommitDistance;
53
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;
59
60     // The children on the standard view.
61     private LinearLayout mTabContents;
62     private TextView mTitleView;
63     private ImageView mFaviconView;
64     private ImageButton mCloseButton;
65
66     // The children on the undo view.
67     private LinearLayout mUndoContents;
68     private Button mUndoButton;
69
70     private Tab mTab;
71     private boolean mCanUndo;
72     private AccessibilityTabModelListItemListener mListener;
73     private final GestureDetector mSwipeGestureDetector;
74     private final int mDefaultHeight;
75     private AccessibilityTabModelListView mCanScrollListener;
76
77     /**
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.
80      */
81     public interface AccessibilityTabModelListItemListener {
82         /**
83          * Called when a user clicks on this list item.
84          * @param tabId The ID of the tab that this list item represents.
85          */
86         public void tabSelected(int tabId);
87
88         /**
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.
91          */
92         public void tabClosed(int tabId);
93
94         /**
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.
97          */
98         public void tabChanged(int tabId);
99
100         /**
101          * @return Whether or not the tab is scheduled to be closed.
102          */
103         public boolean hasPendingClosure(int tabId);
104
105         /**
106          * Schedule a tab to be closed in the future.
107          * @param tabId The ID of the tab to close.
108          */
109         public void schedulePendingClosure(int tabId);
110
111         /**
112          * Cancel a tab's closure.
113          * @param tabId The ID of the tab that should no longer be closed.
114          */
115         public void cancelPendingClosure(int tabId);
116     }
117
118     private final Runnable mCloseRunnable = new Runnable() {
119         @Override
120         public void run() {
121             runCloseAnimation();
122         }
123     };
124
125     private final Handler mHandler = new Handler();
126
127     /**
128      * Used with the swipe away and blink out animations to bring in the undo view.
129      */
130     private final AnimatorListenerAdapter mCloseAnimatorListener =
131             new AnimatorListenerAdapter() {
132         private boolean mIsCancelled;
133
134         @Override
135         public void onAnimationStart(Animator animation) {
136             mIsCancelled = false;
137         }
138
139         @Override
140         public void onAnimationCancel(Animator animation) {
141             mIsCancelled = true;
142         }
143
144         @Override
145         public void onAnimationEnd(Animator animator) {
146             if (mIsCancelled) return;
147
148             mListener.schedulePendingClosure(mTab.getId());
149             setTranslationX(0.f);
150             setScaleX(1.f);
151             setScaleY(1.f);
152             setAlpha(0.f);
153             showUndoView(true);
154             runResetAnimation(false);
155             mHandler.postDelayed(mCloseRunnable, mCloseTimeoutMs);
156         }
157     };
158
159     /**
160      * Used with the close animation to actually close a tab after it has shrunk away.
161      */
162     private final AnimatorListenerAdapter mActuallyCloseAnimatorListener =
163             new AnimatorListenerAdapter() {
164         private boolean mIsCancelled;
165
166         @Override
167         public void onAnimationStart(Animator animation) {
168             mIsCancelled = false;
169         }
170
171         @Override
172         public void onAnimationCancel(Animator animation) {
173             mIsCancelled = true;
174         }
175
176         @Override
177         public void onAnimationEnd(Animator animator) {
178             if (mIsCancelled) return;
179
180             showUndoView(false);
181             setAlpha(1.f);
182             mTabContents.setAlpha(1.f);
183             mUndoContents.setAlpha(1.f);
184             cancelRunningAnimation();
185             mListener.tabClosed(mTab.getId());
186         }
187     };
188
189     /**
190      * @param context The Context to build this widget in.
191      * @param attrs The AttributeSet to use to build this widget.
192      */
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;
199
200         mDefaultHeight =
201                 context.getResources().getDimensionPixelOffset(R.dimen.accessibility_tab_height);
202
203         mCloseAnimationDurationMs = CLOSE_ANIMATION_DURATION_MS;
204         mDefaultAnimationDurationMs = DEFAULT_ANIMATION_DURATION_MS;
205         mCloseTimeoutMs = CLOSE_TIMEOUT_MS;
206     }
207
208     @Override
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);
215
216         mUndoContents = (LinearLayout) findViewById(R.id.undo_contents);
217         mUndoButton = (Button) findViewById(R.id.undo_button);
218
219         setClickable(true);
220         setFocusable(true);
221
222         mCloseButton.setOnClickListener(this);
223         mUndoButton.setOnClickListener(this);
224         setOnClickListener(this);
225     }
226
227     /**
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.
231      */
232     public void setTab(Tab tab, boolean canUndo) {
233         if (mTab != null) mTab.removeObserver(mTabObserver);
234         mTab = tab;
235         tab.addObserver(mTabObserver);
236         mCanUndo = canUndo;
237         updateTabTitle();
238         updateFavicon();
239     }
240
241     private void showUndoView(boolean showView) {
242         if (showView && mCanUndo) {
243             mUndoContents.setVisibility(View.VISIBLE);
244             mTabContents.setVisibility(View.INVISIBLE);
245         } else {
246             mTabContents.setVisibility(View.VISIBLE);
247             mUndoContents.setVisibility(View.INVISIBLE);
248             updateTabTitle();
249             updateFavicon();
250         }
251     }
252
253     /**
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.
256      */
257     public void setListeners(AccessibilityTabModelListItemListener listener,
258             AccessibilityTabModelListView canScrollListener) {
259         mListener = listener;
260         mCanScrollListener = canScrollListener;
261     }
262
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);
267         }
268
269         if (!title.equals(mTitleView.getText())) mTitleView.setText(title);
270
271         String accessibilityString = getContext().getString(R.string.accessibility_tabstrip_tab,
272                 title);
273         if (!accessibilityString.equals(getContentDescription())) {
274             setContentDescription(getContext().getString(R.string.accessibility_tabstrip_tab,
275                     title));
276         }
277     }
278
279     private void updateFavicon() {
280         if (mTab != null) {
281             Bitmap bitmap = mTab.getFavicon();
282             if (bitmap != null) {
283                 mFaviconView.setImageBitmap(bitmap);
284             } else {
285                 mFaviconView.setImageResource(R.drawable.globe_incognito_favicon);
286             }
287         }
288     }
289
290     @Override
291     public void onClick(View v) {
292         if (mListener == null) return;
293
294         int tabId = mTab.getId();
295         if (v == AccessibilityTabModelListItem.this && !mListener.hasPendingClosure(tabId)) {
296             mListener.tabSelected(tabId);
297         } else if (v == mCloseButton) {
298             if (mCanUndo) {
299                 runBlinkOutAnimation();
300             } else {
301                 runCloseAnimation();
302             }
303         } else if (v == mUndoButton) {
304             // Kill the close action.
305             mHandler.removeCallbacks(mCloseRunnable);
306
307             mListener.cancelPendingClosure(tabId);
308             showUndoView(false);
309             setAlpha(0.f);
310             if (mSwipedAway > 0.f) {
311                 setTranslationX(getWidth());
312                 runResetAnimation(false);
313             } else if (mSwipedAway < 0.f) {
314                 setTranslationX(-getWidth());
315                 runResetAnimation(false);
316             } else {
317                 setScaleX(1.2f);
318                 setScaleY(0.f);
319                 runResetAnimation(true);
320             }
321         }
322     }
323
324     @Override
325     protected void onDetachedFromWindow() {
326         super.onDetachedFromWindow();
327         if (mTab != null) mTab.removeObserver(mTabObserver);
328         cancelRunningAnimation();
329     }
330
331     private final TabObserver mTabObserver = new EmptyTabObserver() {
332         @Override
333         public void onFaviconUpdated(Tab tab) {
334             updateFavicon();
335             notifyTabUpdated(tab);
336         }
337
338         @Override
339         public void onTitleUpdated(Tab tab) {
340           updateTabTitle();
341           notifyTabUpdated(tab);
342         }
343
344         @Override
345         public void onUrlUpdated(Tab tab) {
346             updateTabTitle();
347             notifyTabUpdated(tab);
348         }
349     };
350
351     @Override
352     public boolean onTouchEvent(MotionEvent e) {
353         // If there is a pending close task, remove it.
354         mHandler.removeCallbacks(mCloseRunnable);
355
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);
361             } else {
362                 runResetAnimation(false);
363             }
364             mCanScrollListener.setCanScroll(true);
365             return true;
366         }
367         return super.onTouchEvent(e);
368     }
369
370     /**
371      * This call is exposed for the benefit of the animators.
372      *
373      * @param height The height of the current view.
374      */
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);
379         } else {
380             if (params.height == height) return;
381             params.height = height;
382         }
383         setLayoutParams(params);
384     }
385
386     /**
387      * Used to reset the state because views are recycled.
388      */
389     public void resetState() {
390         setTranslationX(0.f);
391         setAlpha(1.f);
392         setScaleX(1.f);
393         setScaleY(1.f);
394         setHeight(mDefaultHeight);
395         cancelRunningAnimation();
396         // Remove any callbacks.
397         mHandler.removeCallbacks(mCloseRunnable);
398
399         if (mListener != null) {
400             boolean hasPendingClosure = mListener.hasPendingClosure(mTab.getId());
401             showUndoView(hasPendingClosure);
402             if (hasPendingClosure) mHandler.postDelayed(mCloseRunnable, mCloseTimeoutMs);
403         } else {
404             showUndoView(false);
405         }
406     }
407
408     /**
409      * Simple gesture listener to catch the scroll and fling gestures on the list item.
410      */
411     private class SwipeGestureListener extends GestureDetector.SimpleOnGestureListener {
412         @Override
413         public boolean onDown(MotionEvent e) {
414             // Returns true so that we can handle events that start with an onDown.
415             return true;
416         }
417
418         @Override
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;
422
423             // Stop the ListView from scrolling vertically.
424             mCanScrollListener.setCanScroll(false);
425
426             float distance = e2.getX() - e1.getX();
427             setTranslationX(distance + getTranslationX());
428             setAlpha(1 - Math.abs(getTranslationX() / getWidth()));
429             return true;
430         }
431
432         @Override
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;
436
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);
442             return true;
443         }
444
445         @Override
446         public boolean onSingleTapConfirmed(MotionEvent e) {
447             performClick();
448             return true;
449         }
450     }
451
452     @VisibleForTesting
453     public void disableAnimations() {
454         mCloseAnimationDurationMs = 0;
455         mDefaultAnimationDurationMs = 0;
456         mCloseTimeoutMs = 0;
457     }
458
459     @VisibleForTesting
460     public boolean hasPendingClosure() {
461         if (mListener != null) return mListener.hasPendingClosure(mTab.getId());
462         return false;
463     }
464
465     private void runSwipeAnimation(long time) {
466         cancelRunningAnimation();
467         mSwipedAway = getTranslationX();
468
469         ObjectAnimator swipe = ObjectAnimator.ofFloat(this, View.TRANSLATION_X,
470                 getTranslationX() > 0 ? getWidth() : -getWidth());
471         ObjectAnimator fadeOut = ObjectAnimator.ofFloat(this, View.ALPHA, 0.f);
472
473         AnimatorSet set = new AnimatorSet();
474         set.playTogether(fadeOut, swipe);
475         set.addListener(mCloseAnimatorListener);
476         set.setDuration(Math.min(time, mDefaultAnimationDurationMs));
477         set.start();
478
479         mActiveAnimation = set;
480     }
481
482     private void runResetAnimation(boolean useCloseAnimationDuration) {
483         cancelRunningAnimation();
484
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);
490
491         AnimatorSet set = new AnimatorSet();
492         set.playTogether(swipe, fadeIn, scaleX, scaleY, resetHeight);
493         set.setDuration(useCloseAnimationDuration
494                 ? mCloseAnimationDurationMs : mDefaultAnimationDurationMs);
495         set.start();
496
497         mActiveAnimation = set;
498     }
499
500     private void runBlinkOutAnimation() {
501         cancelRunningAnimation();
502         mSwipedAway = 0;
503
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);
507
508         AnimatorSet set = new AnimatorSet();
509         set.playTogether(fadeOut, shrinkY, stretchX);
510         set.addListener(mCloseAnimatorListener);
511         set.setDuration(mCloseAnimationDurationMs);
512         set.start();
513
514         mActiveAnimation = set;
515     }
516
517     private void runCloseAnimation() {
518         cancelRunningAnimation();
519
520         ObjectAnimator shrinkHeight = ObjectAnimator.ofInt(this, "height", 0);
521         ObjectAnimator shrinkY = ObjectAnimator.ofFloat(this, View.SCALE_Y, 0.f);
522
523         AnimatorSet set = new AnimatorSet();
524         set.playTogether(shrinkHeight, shrinkY);
525         set.addListener(mActuallyCloseAnimatorListener);
526         set.setDuration(mDefaultAnimationDurationMs);
527         set.start();
528
529         mActiveAnimation = set;
530     }
531
532     private void cancelRunningAnimation() {
533         if (mActiveAnimation != null && mActiveAnimation.isRunning()) mActiveAnimation.cancel();
534
535         mActiveAnimation = null;
536     }
537
538     private void notifyTabUpdated(Tab tab) {
539         if (mListener != null) mListener.tabChanged(tab.getId());
540     }
541 }