mobile/android/base/TabsTray.java

Thu, 22 Jan 2015 13:21:57 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Thu, 22 Jan 2015 13:21:57 +0100
branch
TOR_BUG_9701
changeset 15
b8a032363ba2
permissions
-rw-r--r--

Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6

     1 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
     2 /* This Source Code Form is subject to the terms of the Mozilla Public
     3  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
     4  * You can obtain one at http://mozilla.org/MPL/2.0/. */
     6 package org.mozilla.gecko;
     8 import java.util.ArrayList;
     9 import java.util.List;
    11 import org.mozilla.gecko.animation.PropertyAnimator;
    12 import org.mozilla.gecko.animation.PropertyAnimator.Property;
    13 import org.mozilla.gecko.animation.ViewHelper;
    14 import org.mozilla.gecko.widget.TwoWayView;
    15 import org.mozilla.gecko.widget.TabThumbnailWrapper;
    17 import android.content.Context;
    18 import android.content.res.TypedArray;
    19 import android.graphics.Rect;
    20 import android.graphics.drawable.Drawable;
    21 import android.util.AttributeSet;
    22 import android.view.LayoutInflater;
    23 import android.view.MotionEvent;
    24 import android.view.VelocityTracker;
    25 import android.view.View;
    26 import android.view.ViewConfiguration;
    27 import android.view.ViewGroup;
    28 import android.widget.BaseAdapter;
    29 import android.widget.Button;
    30 import android.widget.ImageButton;
    31 import android.widget.ImageView;
    32 import android.widget.TextView;
    34 public class TabsTray extends TwoWayView
    35     implements TabsPanel.PanelView {
    36     private static final String LOGTAG = "GeckoTabsTray";
    38     private Context mContext;
    39     private TabsPanel mTabsPanel;
    41     private TabsAdapter mTabsAdapter;
    43     private List<View> mPendingClosedTabs;
    44     private int mCloseAnimationCount;
    46     private TabSwipeGestureListener mSwipeListener;
    48     // Time to animate non-flinged tabs of screen, in milliseconds
    49     private static final int ANIMATION_DURATION = 250;
    51     private static final String ABOUT_HOME = "about:home";
    52     private int mOriginalSize = 0;
    54     public TabsTray(Context context, AttributeSet attrs) {
    55         super(context, attrs);
    56         mContext = context;
    58         mCloseAnimationCount = 0;
    59         mPendingClosedTabs = new ArrayList<View>();
    61         setItemsCanFocus(true);
    63         TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.TabsTray);
    64         boolean isPrivate = (a.getInt(R.styleable.TabsTray_tabs, 0x0) == 1);
    65         a.recycle();
    67         mTabsAdapter = new TabsAdapter(mContext, isPrivate);
    68         setAdapter(mTabsAdapter);
    70         mSwipeListener = new TabSwipeGestureListener();
    71         setOnTouchListener(mSwipeListener);
    72         setOnScrollListener(mSwipeListener.makeScrollListener());
    74         setRecyclerListener(new RecyclerListener() {
    75             @Override
    76             public void onMovedToScrapHeap(View view) {
    77                 TabRow row = (TabRow) view.getTag();
    78                 row.thumbnail.setImageDrawable(null);
    79                 row.close.setVisibility(View.VISIBLE);
    80             }
    81         });
    82     }
    84     @Override
    85     public ViewGroup getLayout() {
    86         return this;
    87     }
    89     @Override
    90     public void setTabsPanel(TabsPanel panel) {
    91         mTabsPanel = panel;
    92     }
    94     @Override
    95     public void show() {
    96         setVisibility(View.VISIBLE);
    97         Tabs.getInstance().refreshThumbnails();
    98         Tabs.registerOnTabsChangedListener(mTabsAdapter);
    99         mTabsAdapter.refreshTabsData();
   100     }
   102     @Override
   103     public void hide() {
   104         setVisibility(View.GONE);
   105         Tabs.unregisterOnTabsChangedListener(mTabsAdapter);
   106         GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Tab:Screenshot:Cancel",""));
   107         mTabsAdapter.clear();
   108     }
   110     @Override
   111     public boolean shouldExpand() {
   112         return isVertical();
   113     }
   115     private void autoHidePanel() {
   116         mTabsPanel.autoHidePanel();
   117     }
   119     // ViewHolder for a row in the list
   120     private class TabRow {
   121         int id;
   122         TextView title;
   123         ImageView thumbnail;
   124         ImageButton close;
   125         ViewGroup info;
   126         TabThumbnailWrapper thumbnailWrapper;
   128         public TabRow(View view) {
   129             info = (ViewGroup) view;
   130             title = (TextView) view.findViewById(R.id.title);
   131             thumbnail = (ImageView) view.findViewById(R.id.thumbnail);
   132             close = (ImageButton) view.findViewById(R.id.close);
   133             thumbnailWrapper = (TabThumbnailWrapper) view.findViewById(R.id.wrapper);
   134         }
   135     }
   137     // Adapter to bind tabs into a list
   138     private class TabsAdapter extends BaseAdapter implements Tabs.OnTabsChangedListener {
   139         private Context mContext;
   140         private boolean mIsPrivate;
   141         private ArrayList<Tab> mTabs;
   142         private LayoutInflater mInflater;
   143         private Button.OnClickListener mOnCloseClickListener;
   145         public TabsAdapter(Context context, boolean isPrivate) {
   146             mContext = context;
   147             mInflater = LayoutInflater.from(mContext);
   148             mIsPrivate = isPrivate;
   150             mOnCloseClickListener = new Button.OnClickListener() {
   151                 @Override
   152                 public void onClick(View v) {
   153                     TabRow tab = (TabRow) v.getTag();
   154                     final int pos = (isVertical() ? tab.info.getWidth() : tab.info.getHeight());
   155                     animateClose(tab.info, pos);
   156                 }
   157             };
   158         }
   160         @Override
   161         public void onTabChanged(Tab tab, Tabs.TabEvents msg, Object data) {
   162             switch (msg) {
   163                 case ADDED:
   164                     // Refresh the list to make sure the new tab is added in the right position.
   165                     refreshTabsData();
   166                     break;
   168                 case CLOSED:
   169                     removeTab(tab);
   170                     break;
   172                 case SELECTED:
   173                     // Update the selected position, then fall through...
   174                     updateSelectedPosition();
   175                 case UNSELECTED:
   176                     // We just need to update the style for the unselected tab...
   177                 case THUMBNAIL:
   178                 case TITLE:
   179                 case RECORDING_CHANGE:
   180                     View view = TabsTray.this.getChildAt(getPositionForTab(tab) - TabsTray.this.getFirstVisiblePosition());
   181                     if (view == null)
   182                         return;
   184                     TabRow row = (TabRow) view.getTag();
   185                     assignValues(row, tab);
   186                     break;
   187             }
   188         }
   190         private void refreshTabsData() {
   191             // Store a different copy of the tabs, so that we don't have to worry about
   192             // accidentally updating it on the wrong thread.
   193             mTabs = new ArrayList<Tab>();
   195             Iterable<Tab> tabs = Tabs.getInstance().getTabsInOrder();
   196             for (Tab tab : tabs) {
   197                 if (tab.isPrivate() == mIsPrivate)
   198                     mTabs.add(tab);
   199             }
   201             notifyDataSetChanged(); // Be sure to call this whenever mTabs changes.
   202             updateSelectedPosition();
   203         }
   205         // Updates the selected position in the list so that it will be scrolled to the right place.
   206         private void updateSelectedPosition() {
   207             int selected = getPositionForTab(Tabs.getInstance().getSelectedTab());
   208             updateSelectedStyle(selected);
   210             if (selected != -1) {
   211                 TabsTray.this.setSelection(selected);
   212             }
   213         }
   215         /**
   216          * Updates the selected/unselected style for the tabs.
   217          *
   218          * @param selected position of the selected tab
   219          */
   220         private void updateSelectedStyle(int selected) {
   221             for (int i = 0; i < getCount(); i++) {
   222                 TabsTray.this.setItemChecked(i, (i == selected));
   223             }
   224         }
   226         public void clear() {
   227             mTabs = null;
   228             notifyDataSetChanged(); // Be sure to call this whenever mTabs changes.
   229         }
   231         @Override
   232         public int getCount() {
   233             return (mTabs == null ? 0 : mTabs.size());
   234         }
   236         @Override
   237         public Tab getItem(int position) {
   238             return mTabs.get(position);
   239         }
   241         @Override
   242         public long getItemId(int position) {
   243             return position;
   244         }
   246         private int getPositionForTab(Tab tab) {
   247             if (mTabs == null || tab == null)
   248                 return -1;
   250             return mTabs.indexOf(tab);
   251         }
   253         private void removeTab(Tab tab) {
   254             if (tab.isPrivate() == mIsPrivate && mTabs != null) {
   255                 mTabs.remove(tab);
   256                 notifyDataSetChanged(); // Be sure to call this whenever mTabs changes.
   258                 int selected = getPositionForTab(Tabs.getInstance().getSelectedTab());
   259                 updateSelectedStyle(selected);
   260             }
   261         }
   263         private void assignValues(TabRow row, Tab tab) {
   264             if (row == null || tab == null)
   265                 return;
   267             row.id = tab.getId();
   269             Drawable thumbnailImage = tab.getThumbnail();
   270             if (thumbnailImage != null) {
   271                 row.thumbnail.setImageDrawable(thumbnailImage);
   272             } else if (AboutPages.isAboutHome(tab.getURL())) {
   273                 row.thumbnail.setImageResource(R.drawable.abouthome_thumbnail);
   274             } else {
   275                 row.thumbnail.setImageResource(R.drawable.tab_thumbnail_default);
   276             }
   277             if (row.thumbnailWrapper != null) {
   278                 row.thumbnailWrapper.setRecording(tab.isRecording());
   279             }
   280             row.title.setText(tab.getDisplayTitle());
   281             row.close.setTag(row);
   282         }
   284         private void resetTransforms(View view) {
   285             ViewHelper.setAlpha(view, 1);
   286             if (mOriginalSize == 0)
   287                 return;
   289             if (isVertical()) {
   290                 ViewHelper.setHeight(view, mOriginalSize);
   291                 ViewHelper.setTranslationX(view, 0);
   292             } else {
   293                 ViewHelper.setWidth(view, mOriginalSize);
   294                 ViewHelper.setTranslationY(view, 0);
   295             }
   296         }
   298         @Override
   299         public View getView(int position, View convertView, ViewGroup parent) {
   300             TabRow row;
   302             if (convertView == null) {
   303                 convertView = mInflater.inflate(R.layout.tabs_row, null);
   304                 row = new TabRow(convertView);
   305                 row.close.setOnClickListener(mOnCloseClickListener);
   306                 convertView.setTag(row);
   307             } else {
   308                 row = (TabRow) convertView.getTag();
   309                 // If we're recycling this view, there's a chance it was transformed during
   310                 // the close animation. Remove any of those properties.
   311                 resetTransforms(convertView);
   312             }
   314             Tab tab = mTabs.get(position);
   315             assignValues(row, tab);
   317             return convertView;
   318         }
   319     }
   321     private boolean isVertical() {
   322         return (getOrientation().compareTo(TwoWayView.Orientation.VERTICAL) == 0);
   323     }
   325     private void animateClose(final View view, int pos) {
   326         PropertyAnimator animator = new PropertyAnimator(ANIMATION_DURATION);
   327         animator.attach(view, Property.ALPHA, 0);
   329         if (isVertical())
   330             animator.attach(view, Property.TRANSLATION_X, pos);
   331         else
   332             animator.attach(view, Property.TRANSLATION_Y, pos);
   334         mCloseAnimationCount++;
   335         mPendingClosedTabs.add(view);
   337         animator.addPropertyAnimationListener(new PropertyAnimator.PropertyAnimationListener() {
   338             @Override
   339             public void onPropertyAnimationStart() { }
   340             @Override
   341             public void onPropertyAnimationEnd() {
   342                 mCloseAnimationCount--;
   343                 if (mCloseAnimationCount > 0)
   344                     return;
   346                 for (View pendingView : mPendingClosedTabs) {
   347                     animateFinishClose(pendingView);
   348                 }
   350                 mPendingClosedTabs.clear();
   351             }
   352         });
   354         if (mTabsAdapter.getCount() == 1)
   355             autoHidePanel();
   357         animator.start();
   358     }
   360     private void animateFinishClose(final View view) {
   361         PropertyAnimator animator = new PropertyAnimator(ANIMATION_DURATION);
   363         final boolean isVertical = isVertical();
   364         if (isVertical)
   365             animator.attach(view, Property.HEIGHT, 1);
   366         else
   367             animator.attach(view, Property.WIDTH, 1);
   369         TabRow tab = (TabRow)view.getTag();
   370         final int tabId = tab.id;
   371         // Caching this assumes that all rows are the same height
   372 	if (mOriginalSize == 0)
   373             mOriginalSize = (isVertical ? view.getHeight() : view.getWidth());
   375         animator.addPropertyAnimationListener(new PropertyAnimator.PropertyAnimationListener() {
   376             @Override
   377             public void onPropertyAnimationStart() { }
   378             @Override
   379             public void onPropertyAnimationEnd() {
   380                 Tabs tabs = Tabs.getInstance();
   381                 Tab tab = tabs.getTab(tabId);
   382                 tabs.closeTab(tab);
   383             }
   384         });
   386         animator.start();
   387     }
   389     private void animateCancel(final View view) {
   390         PropertyAnimator animator = new PropertyAnimator(ANIMATION_DURATION);
   391         animator.attach(view, Property.ALPHA, 1);
   393         if (isVertical())
   394             animator.attach(view, Property.TRANSLATION_X, 0);
   395         else
   396             animator.attach(view, Property.TRANSLATION_Y, 0);
   399         animator.addPropertyAnimationListener(new PropertyAnimator.PropertyAnimationListener() {
   400             @Override
   401             public void onPropertyAnimationStart() { }
   402             @Override
   403             public void onPropertyAnimationEnd() {
   404                 TabRow tab = (TabRow) view.getTag();
   405                 tab.close.setVisibility(View.VISIBLE);
   406             }
   407         });
   409         animator.start();
   410     }
   412     private class TabSwipeGestureListener implements View.OnTouchListener {
   413         // same value the stock browser uses for after drag animation velocity in pixels/sec
   414         // http://androidxref.com/4.0.4/xref/packages/apps/Browser/src/com/android/browser/NavTabScroller.java#61
   415         private static final float MIN_VELOCITY = 750;
   417         private int mSwipeThreshold;
   418         private int mMinFlingVelocity;
   420         private int mMaxFlingVelocity;
   421         private VelocityTracker mVelocityTracker;
   423         private int mListWidth = 1;
   424         private int mListHeight = 1;
   426         private View mSwipeView;
   427         private int mSwipeViewPosition;
   428         private Runnable mPendingCheckForTap;
   430         private float mSwipeStartX;
   431         private float mSwipeStartY;
   432         private boolean mSwiping;
   433         private boolean mEnabled;
   435         public TabSwipeGestureListener() {
   436             mSwipeView = null;
   437             mSwipeViewPosition = TwoWayView.INVALID_POSITION;
   438             mSwiping = false;
   439             mEnabled = true;
   441             ViewConfiguration vc = ViewConfiguration.get(TabsTray.this.getContext());
   442             mSwipeThreshold = vc.getScaledTouchSlop();
   443             mMinFlingVelocity = (int) (getContext().getResources().getDisplayMetrics().density * MIN_VELOCITY);
   444             mMaxFlingVelocity = vc.getScaledMaximumFlingVelocity();
   445         }
   447         public void setEnabled(boolean enabled) {
   448             mEnabled = enabled;
   449         }
   451         public TwoWayView.OnScrollListener makeScrollListener() {
   452             return new TwoWayView.OnScrollListener() {
   453                 @Override
   454                 public void onScrollStateChanged(TwoWayView twoWayView, int scrollState) {
   455                     setEnabled(scrollState != TwoWayView.OnScrollListener.SCROLL_STATE_TOUCH_SCROLL);
   456                 }
   458                 @Override
   459                 public void onScroll(TwoWayView twoWayView, int i, int i1, int i2) {
   460                 }
   461             };
   462         }
   464         @Override
   465         public boolean onTouch(View view, MotionEvent e) {
   466             if (!mEnabled)
   467                 return false;
   469             if (mListWidth < 2 || mListHeight < 2) {
   470                 mListWidth = TabsTray.this.getWidth();
   471                 mListHeight = TabsTray.this.getHeight();
   472             }
   474             switch (e.getActionMasked()) {
   475                 case MotionEvent.ACTION_DOWN: {
   476                     // Check if we should set pressed state on the
   477                     // touched view after a standard delay.
   478                     triggerCheckForTap();
   480                     final float x = e.getRawX();
   481                     final float y = e.getRawY();
   483                     // Find out which view is being touched
   484                     mSwipeView = findViewAt(x, y);
   486                     if (mSwipeView != null) {
   487                         mSwipeStartX = e.getRawX();
   488                         mSwipeStartY = e.getRawY();
   489                         mSwipeViewPosition = TabsTray.this.getPositionForView(mSwipeView);
   491                         mVelocityTracker = VelocityTracker.obtain();
   492                         mVelocityTracker.addMovement(e);
   493                     }
   495                     view.onTouchEvent(e);
   496                     return true;
   497                 }
   499                 case MotionEvent.ACTION_UP: {
   500                     if (mSwipeView == null)
   501                         break;
   503                     cancelCheckForTap();
   504                     mSwipeView.setPressed(false);
   506                     if (!mSwiping) {
   507                         TabRow tab = (TabRow) mSwipeView.getTag();
   508                         Tabs.getInstance().selectTab(tab.id);
   509                         autoHidePanel();
   510                         break;
   511                     }
   513                     mVelocityTracker.addMovement(e);
   514                     mVelocityTracker.computeCurrentVelocity(1000, mMaxFlingVelocity);
   516                     float velocityX = Math.abs(mVelocityTracker.getXVelocity());
   517                     float velocityY = Math.abs(mVelocityTracker.getYVelocity());
   519                     boolean dismiss = false;
   520                     boolean dismissDirection = false;
   521                     int dismissTranslation = 0;
   523                     if (isVertical()) {
   524                         float deltaX = ViewHelper.getTranslationX(mSwipeView);
   526                         if (Math.abs(deltaX) > mListWidth / 2) {
   527                             dismiss = true;
   528                             dismissDirection = (deltaX > 0);
   529                         } else if (mMinFlingVelocity <= velocityX && velocityX <= mMaxFlingVelocity
   530                                 && velocityY < velocityX) {
   531                             dismiss = mSwiping && (deltaX * mVelocityTracker.getXVelocity() > 0);
   532                             dismissDirection = (mVelocityTracker.getXVelocity() > 0);
   533                         }
   535                         dismissTranslation = (dismissDirection ? mListWidth : -mListWidth);
   536                     } else {
   537                         float deltaY = ViewHelper.getTranslationY(mSwipeView);
   539                         if (Math.abs(deltaY) > mListHeight / 2) {
   540                             dismiss = true;
   541                             dismissDirection = (deltaY > 0);
   542                         } else if (mMinFlingVelocity <= velocityY && velocityY <= mMaxFlingVelocity
   543                                 && velocityX < velocityY) {
   544                             dismiss = mSwiping && (deltaY * mVelocityTracker.getYVelocity() > 0);
   545                             dismissDirection = (mVelocityTracker.getYVelocity() > 0);
   546                         }
   548                         dismissTranslation = (dismissDirection ? mListHeight : -mListHeight);
   549                      }
   551                     if (dismiss)
   552                         animateClose(mSwipeView, dismissTranslation);
   553                     else
   554                         animateCancel(mSwipeView);
   556                     mVelocityTracker = null;
   557                     mSwipeView = null;
   558                     mSwipeViewPosition = TwoWayView.INVALID_POSITION;
   560                     mSwipeStartX = 0;
   561                     mSwipeStartY = 0;
   562                     mSwiping = false;
   564                     break;
   565                 }
   567                 case MotionEvent.ACTION_MOVE: {
   568                     if (mSwipeView == null)
   569                         break;
   571                     mVelocityTracker.addMovement(e);
   573                     final boolean isVertical = isVertical();
   575                     float deltaX = e.getRawX() - mSwipeStartX;
   576                     float deltaY = e.getRawY() - mSwipeStartY;
   577                     float delta = (isVertical ? deltaX : deltaY);
   579                     boolean isScrollingX = Math.abs(deltaX) > mSwipeThreshold;
   580                     boolean isScrollingY = Math.abs(deltaY) > mSwipeThreshold;
   581                     boolean isSwipingToClose = (isVertical ? isScrollingX : isScrollingY);
   583                     // If we're actually swiping, make sure we don't
   584                     // set pressed state on the swiped view.
   585                     if (isScrollingX || isScrollingY)
   586                         cancelCheckForTap();
   588                     if (isSwipingToClose) {
   589                         mSwiping = true;
   590                         TabsTray.this.requestDisallowInterceptTouchEvent(true);
   592                         TabRow tab = (TabRow) mSwipeView.getTag();
   593                         tab.close.setVisibility(View.INVISIBLE);
   595                         // Stops listview from highlighting the touched item
   596                         // in the list when swiping.
   597                         MotionEvent cancelEvent = MotionEvent.obtain(e);
   598                         cancelEvent.setAction(MotionEvent.ACTION_CANCEL |
   599                                 (e.getActionIndex() << MotionEvent.ACTION_POINTER_INDEX_SHIFT));
   600                         TabsTray.this.onTouchEvent(cancelEvent);
   601                     }
   603                     if (mSwiping) {
   604                         if (isVertical)
   605                             ViewHelper.setTranslationX(mSwipeView, delta);
   606                         else
   607                             ViewHelper.setTranslationY(mSwipeView, delta);
   609                         ViewHelper.setAlpha(mSwipeView, Math.max(0.1f, Math.min(1f,
   610                                 1f - 2f * Math.abs(delta) / (isVertical ? mListWidth : mListHeight))));
   612                         return true;
   613                     }
   615                     break;
   616                 }
   617             }
   619             return false;
   620         }
   622         private View findViewAt(float rawX, float rawY) {
   623             Rect rect = new Rect();
   625             int[] listViewCoords = new int[2];
   626             TabsTray.this.getLocationOnScreen(listViewCoords);
   628             int x = (int) rawX - listViewCoords[0];
   629             int y = (int) rawY - listViewCoords[1];
   631             for (int i = 0; i < TabsTray.this.getChildCount(); i++) {
   632                 View child = TabsTray.this.getChildAt(i);
   633                 child.getHitRect(rect);
   635                 if (rect.contains(x, y))
   636                     return child;
   637             }
   639             return null;
   640         }
   642         private void triggerCheckForTap() {
   643             if (mPendingCheckForTap == null)
   644                 mPendingCheckForTap = new CheckForTap();
   646             TabsTray.this.postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
   647         }
   649         private void cancelCheckForTap() {
   650             if (mPendingCheckForTap == null)
   651                 return;
   653             TabsTray.this.removeCallbacks(mPendingCheckForTap);
   654         }
   656         private class CheckForTap implements Runnable {
   657             @Override
   658             public void run() {
   659                 if (!mSwiping && mSwipeView != null && mEnabled)
   660                     mSwipeView.setPressed(true);
   661             }
   662         }
   663     }
   664 }

mercurial