/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this file, * You can obtain one at http://mozilla.org/MPL/2.0/. */ package org.mozilla.gecko; import org.mozilla.gecko.PropertyAnimator.Property; import android.content.Context; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.text.TextUtils; import android.util.AttributeSet; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.VelocityTracker; import android.view.View; import android.view.ViewConfiguration; import android.view.ViewGroup; import android.widget.AbsListView; import android.widget.AbsListView.RecyclerListener; import android.widget.BaseAdapter; import android.widget.Button; import android.widget.ImageButton; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.ListView; import android.widget.TextView; import java.util.ArrayList; public class TabsTray extends LinearLayout implements TabsPanel.PanelView { private static final String LOGTAG = "GeckoTabsTray"; private Context mContext; private TabsPanel mTabsPanel; private static ListView mList; private TabsAdapter mTabsAdapter; private boolean mWaitingForClose; private TabSwipeGestureListener mSwipeListener; // Time to animate non-flinged tabs of screen, in milliseconds private static final int ANIMATION_DURATION = 250; private static final String ABOUT_HOME = "about:home"; public TabsTray(Context context, AttributeSet attrs) { super(context, attrs); mContext = context; LayoutInflater.from(context).inflate(R.layout.tabs_tray, this); mList = (ListView) findViewById(R.id.list); mList.setItemsCanFocus(true); mTabsAdapter = new TabsAdapter(mContext); mList.setAdapter(mTabsAdapter); mSwipeListener = new TabSwipeGestureListener(mList); mList.setOnTouchListener(mSwipeListener); mList.setOnScrollListener(mSwipeListener.makeScrollListener()); mList.setRecyclerListener(new RecyclerListener() { @Override public void onMovedToScrapHeap(View view) { TabRow row = (TabRow) view.getTag(); row.thumbnail.setImageDrawable(null); } }); } @Override public ViewGroup getLayout() { return this; } @Override public void setTabsPanel(TabsPanel panel) { mTabsPanel = panel; } @Override public void show() { mWaitingForClose = false; Tabs.getInstance().refreshThumbnails(); Tabs.registerOnTabsChangedListener(mTabsAdapter); mTabsAdapter.refreshTabsData(); } @Override public void hide() { Tabs.unregisterOnTabsChangedListener(mTabsAdapter); GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Tab:Screenshot:Cancel","")); mTabsAdapter.clear(); } void autoHidePanel() { mTabsPanel.autoHidePanel(); } // ViewHolder for a row in the list private class TabRow { int id; TextView title; ImageView thumbnail; ImageButton close; LinearLayout info; public TabRow(View view) { info = (LinearLayout) view; title = (TextView) view.findViewById(R.id.title); thumbnail = (ImageView) view.findViewById(R.id.thumbnail); close = (ImageButton) view.findViewById(R.id.close); } } // Adapter to bind tabs into a list private class TabsAdapter extends BaseAdapter implements Tabs.OnTabsChangedListener { private Context mContext; private ArrayList mTabs; private LayoutInflater mInflater; private Button.OnClickListener mOnCloseClickListener; public TabsAdapter(Context context) { mContext = context; mInflater = LayoutInflater.from(mContext); mOnCloseClickListener = new Button.OnClickListener() { public void onClick(View v) { TabRow tab = (TabRow) v.getTag(); animateClose(tab.info, tab.info.getWidth()); } }; } public void onTabChanged(Tab tab, Tabs.TabEvents msg, Object data) { switch (msg) { case ADDED: // Refresh the list to make sure the new tab is added in the right position. refreshTabsData(); break; case CLOSED: mWaitingForClose = false; removeTab(tab); break; case SELECTED: // Update the selected position, then fall through... updateSelectedPosition(); case UNSELECTED: // We just need to update the style for the unselected tab... case THUMBNAIL: case TITLE: View view = mList.getChildAt(getPositionForTab(tab) - mList.getFirstVisiblePosition()); if (view == null) return; TabRow row = (TabRow) view.getTag(); assignValues(row, tab); break; } } private void refreshTabsData() { // Store a different copy of the tabs, so that we don't have to worry about // accidentally updating it on the wrong thread. mTabs = new ArrayList(); Iterable tabs = Tabs.getInstance().getTabsInOrder(); for (Tab tab : tabs) { mTabs.add(tab); } notifyDataSetChanged(); // Be sure to call this whenever mTabs changes. updateSelectedPosition(); } // Updates the selected position in the list so that it will be scrolled to the right place. private void updateSelectedPosition() { int selected = getPositionForTab(Tabs.getInstance().getSelectedTab()); if (selected == -1) return; mList.setSelection(selected); } public void clear() { mTabs = null; notifyDataSetChanged(); // Be sure to call this whenever mTabs changes. } public int getCount() { return (mTabs == null ? 0 : mTabs.size()); } public Tab getItem(int position) { return mTabs.get(position); } public long getItemId(int position) { return position; } private int getPositionForTab(Tab tab) { if (mTabs == null || tab == null) return -1; return mTabs.indexOf(tab); } private void removeTab(Tab tab) { mTabs.remove(tab); notifyDataSetChanged(); // Be sure to call this whenever mTabs changes. } private void assignValues(TabRow row, Tab tab) { if (row == null || tab == null) return; row.id = tab.getId(); Drawable thumbnailImage = tab.getThumbnail(); if (thumbnailImage != null) row.thumbnail.setImageDrawable(thumbnailImage); else if (TextUtils.equals(tab.getURL(), ABOUT_HOME)) row.thumbnail.setImageResource(R.drawable.abouthome_thumbnail); else row.thumbnail.setImageResource(R.drawable.tab_thumbnail_default); if (Tabs.getInstance().isSelectedTab(tab)) row.info.setBackgroundResource(R.drawable.tabs_tray_active_selector); else row.info.setBackgroundResource(R.drawable.tabs_tray_default_selector); row.title.setText(tab.getDisplayTitle()); row.close.setTag(row); row.close.setVisibility(mTabs.size() > 1 ? View.VISIBLE : View.INVISIBLE); } public View getView(int position, View convertView, ViewGroup parent) { TabRow row; if (convertView == null) { convertView = mInflater.inflate(R.layout.tabs_row, null); row = new TabRow(convertView); row.close.setOnClickListener(mOnCloseClickListener); convertView.setTag(row); } else { row = (TabRow) convertView.getTag(); } Tab tab = mTabs.get(position); assignValues(row, tab); return convertView; } } private boolean hasOnlyOneTab() { return (mTabsAdapter != null && mTabsAdapter.getCount() == 1); } private void animateClose(final View view, int x) { // Just bail out, if we're already closing if (mWaitingForClose) return; PropertyAnimator animator = new PropertyAnimator(ANIMATION_DURATION); animator.attach(view, Property.ALPHA, 0); animator.attach(view, Property.TRANSLATION_X, x); mWaitingForClose = true; animator.setPropertyAnimationListener(new PropertyAnimator.PropertyAnimationListener() { public void onPropertyAnimationStart() { } public void onPropertyAnimationEnd() { animateFinishClose(view); } }); animator.start(); } private void animateFinishClose(final View view) { PropertyAnimator animator = new PropertyAnimator(ANIMATION_DURATION); animator.attach(view, Property.HEIGHT, 1); TabRow tab = (TabRow)view.getTag(); final int tabId = tab.id; final int originalHeight = view.getHeight(); animator.setPropertyAnimationListener(new PropertyAnimator.PropertyAnimationListener() { public void onPropertyAnimationStart() { } public void onPropertyAnimationEnd() { // Reset view presentation as it will be recycled in the // list view by the adapter. AnimatorProxy proxy = AnimatorProxy.create(view); proxy.setAlpha(1); proxy.setTranslationX(0); proxy.setHeight(originalHeight); Tabs tabs = Tabs.getInstance(); Tab tab = tabs.getTab(tabId); tabs.closeTab(tab); } }); animator.start(); } private void animateCancel(final View view) { PropertyAnimator animator = new PropertyAnimator(ANIMATION_DURATION); animator.attach(view, Property.ALPHA, 1); animator.attach(view, Property.TRANSLATION_X, 0); animator.setPropertyAnimationListener(new PropertyAnimator.PropertyAnimationListener() { public void onPropertyAnimationStart() { } public void onPropertyAnimationEnd() { if (!hasOnlyOneTab()) { TabRow tab = (TabRow) view.getTag(); tab.close.setVisibility(View.VISIBLE); } } }); animator.start(); } private class TabSwipeGestureListener implements View.OnTouchListener { private int mSwipeThreshold; private int mMinFlingVelocity; private int mMaxFlingVelocity; private VelocityTracker mVelocityTracker; private ListView mListView; private int mListWidth = 1; private View mSwipeView; private AnimatorProxy mSwipeProxy; private int mSwipeViewPosition; private Runnable mPendingCheckForTap; private float mSwipeStart; private boolean mSwiping; private boolean mEnabled; public TabSwipeGestureListener(ListView listView) { mListView = listView; mSwipeView = null; mSwipeProxy = null; mSwipeViewPosition = ListView.INVALID_POSITION; mSwiping = false; mEnabled = true; ViewConfiguration vc = ViewConfiguration.get(listView.getContext()); mSwipeThreshold = vc.getScaledTouchSlop(); mMinFlingVelocity = vc.getScaledMinimumFlingVelocity(); mMaxFlingVelocity = vc.getScaledMaximumFlingVelocity(); } public void setEnabled(boolean enabled) { mEnabled = enabled; } public AbsListView.OnScrollListener makeScrollListener() { return new AbsListView.OnScrollListener() { @Override public void onScrollStateChanged(AbsListView absListView, int scrollState) { setEnabled(scrollState != AbsListView.OnScrollListener.SCROLL_STATE_TOUCH_SCROLL); } @Override public void onScroll(AbsListView absListView, int i, int i1, int i2) { } }; } @Override public boolean onTouch(View view, MotionEvent e) { if (!mEnabled) return false; if (mListWidth < 2) mListWidth = mListView.getWidth(); switch (e.getActionMasked()) { case MotionEvent.ACTION_DOWN: { // Check if we should set pressed state on the // touched view after a standard delay. triggerCheckForTap(); // Find out which view is being touched mSwipeView = findViewAt(e.getRawX(), e.getRawY()); if (mSwipeView != null) { mSwipeStart = e.getRawX(); mSwipeViewPosition = mListView.getPositionForView(mSwipeView); mVelocityTracker = VelocityTracker.obtain(); mVelocityTracker.addMovement(e); } view.onTouchEvent(e); return true; } case MotionEvent.ACTION_UP: { if (mSwipeView == null) break; mSwipeView.setPressed(false); if (!mSwiping) { TabRow tab = (TabRow) mSwipeView.getTag(); Tabs.getInstance().selectTab(tab.id); autoHidePanel(); break; } float deltaX = mSwipeProxy.getTranslationX(); mVelocityTracker.addMovement(e); mVelocityTracker.computeCurrentVelocity(1000, mMaxFlingVelocity); float velocityX = Math.abs(mVelocityTracker.getXVelocity()); float velocityY = Math.abs(mVelocityTracker.getYVelocity()); boolean dismiss = false; boolean dismissRight = false; if (Math.abs(deltaX) > mListWidth / 2) { dismiss = true; dismissRight = (deltaX > 0); } else if (mMinFlingVelocity <= velocityX && velocityX <= mMaxFlingVelocity && velocityY < velocityX) { dismiss = mSwiping && !hasOnlyOneTab() && (deltaX * mVelocityTracker.getXVelocity() > 0); dismissRight = (mVelocityTracker.getXVelocity() > 0); } if (dismiss) animateClose(mSwipeView, (dismissRight ? mListWidth : -mListWidth)); else animateCancel(mSwipeView); mVelocityTracker = null; mSwipeView = null; mSwipeViewPosition = ListView.INVALID_POSITION; mSwipeProxy = null; mSwipeStart = 0; mSwiping = false; break; } case MotionEvent.ACTION_MOVE: { if (mSwipeView == null) break; mVelocityTracker.addMovement(e); float deltaX = e.getRawX() - mSwipeStart; if (Math.abs(deltaX) > mSwipeThreshold) { // If we're actually swiping, make sure we don't // set pressed state on the swiped view. cancelCheckForTap(); mSwiping = true; mListView.requestDisallowInterceptTouchEvent(true); TabRow tab = (TabRow) mSwipeView.getTag(); tab.close.setVisibility(View.INVISIBLE); // Stops listview from highlighting the touched item // in the list when swiping. MotionEvent cancelEvent = MotionEvent.obtain(e); cancelEvent.setAction(MotionEvent.ACTION_CANCEL | (e.getActionIndex() << MotionEvent.ACTION_POINTER_INDEX_SHIFT)); mListView.onTouchEvent(cancelEvent); mSwipeProxy = AnimatorProxy.create(mSwipeView); } if (mSwiping) { if (hasOnlyOneTab()) { mSwipeProxy.setTranslationX(deltaX / 4); } else { mSwipeProxy.setTranslationX(deltaX); mSwipeProxy.setAlpha(Math.max(0.1f, Math.min(1f, 1f - 2f * Math.abs(deltaX) / mListWidth))); } return true; } break; } } return false; } private View findViewAt(float rawX, float rawY) { if (mList == null) return null; Rect rect = new Rect(); int[] listViewCoords = new int[2]; mListView.getLocationOnScreen(listViewCoords); int x = (int) rawX - listViewCoords[0]; int y = (int) rawY - listViewCoords[1]; for (int i = 0; i < mListView.getChildCount(); i++) { View child = mListView.getChildAt(i); child.getHitRect(rect); if (rect.contains(x, y)) return child; } return null; } private void triggerCheckForTap() { if (mPendingCheckForTap == null) mPendingCheckForTap = new CheckForTap(); mListView.postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout()); } private void cancelCheckForTap() { if (mPendingCheckForTap == null) return; mListView.removeCallbacks(mPendingCheckForTap); } private class CheckForTap implements Runnable { @Override public void run() { if (!mSwiping && mSwipeView != null && mEnabled) mSwipeView.setPressed(true); } } } }