Files
palemoon27/mobile/android/base/tabs/TabsGridLayout.java
T
2018-07-24 23:11:02 +08:00

419 lines
16 KiB
Java

/* -*- 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.goanna.tabs;
import java.util.ArrayList;
import java.util.List;
import org.mozilla.goanna.animation.ViewHelper;
import org.mozilla.goanna.GoannaAppShell;
import org.mozilla.goanna.GoannaEvent;
import org.mozilla.goanna.R;
import org.mozilla.goanna.Tab;
import org.mozilla.goanna.tabs.TabsPanel.TabsLayout;
import org.mozilla.goanna.Tabs;
import android.content.Context;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.PointF;
import android.util.AttributeSet;
import android.util.SparseArray;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.view.animation.DecelerateInterpolator;
import android.widget.AdapterView;
import android.widget.Button;
import android.widget.GridView;
import com.nineoldandroids.animation.Animator;
import com.nineoldandroids.animation.AnimatorSet;
import com.nineoldandroids.animation.ObjectAnimator;
import com.nineoldandroids.animation.PropertyValuesHolder;
import com.nineoldandroids.animation.ValueAnimator;
/**
* A tabs layout implementation for the tablet redesign (bug 1014156).
* Expected to replace TabsListLayout once complete.
*/
class TabsGridLayout extends GridView
implements TabsLayout,
Tabs.OnTabsChangedListener {
private static final String LOGTAG = "Goanna" + TabsGridLayout.class.getSimpleName();
private static final int ANIM_TIME_MS = 200;
public static final int ANIM_DELAY_MULTIPLE_MS = 20;
private static final DecelerateInterpolator ANIM_INTERPOLATOR = new DecelerateInterpolator();
private final Context mContext;
private TabsPanel mTabsPanel;
private final SparseArray<PointF> mTabLocations = new SparseArray<PointF>();
final private boolean mIsPrivate;
private final TabsLayoutAdapter mTabsAdapter;
private final int mColumnWidth;
public TabsGridLayout(Context context, AttributeSet attrs) {
super(context, attrs, R.attr.tabGridLayoutViewStyle);
mContext = context;
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.TabsLayout);
mIsPrivate = (a.getInt(R.styleable.TabsLayout_tabs, 0x0) == 1);
a.recycle();
mTabsAdapter = new TabsGridLayoutAdapter(mContext);
setAdapter(mTabsAdapter);
setRecyclerListener(new RecyclerListener() {
@Override
public void onMovedToScrapHeap(View view) {
TabsLayoutItemView item = (TabsLayoutItemView) view;
item.setThumbnail(null);
}
});
setScrollBarStyle(View.SCROLLBARS_OUTSIDE_OVERLAY);
setStretchMode(GridView.STRETCH_SPACING);
setGravity(Gravity.CENTER);
setNumColumns(GridView.AUTO_FIT);
// The clipToPadding setting in the styles.xml doesn't seem to be working (bug 1101784)
// so lets set it manually in code for the moment as it's needed for the padding animation
setClipToPadding(false);
final Resources resources = getResources();
mColumnWidth = resources.getDimensionPixelSize(R.dimen.new_tablet_tab_panel_column_width);
setColumnWidth(mColumnWidth);
final int padding = resources.getDimensionPixelSize(R.dimen.new_tablet_tab_panel_grid_padding);
final int paddingTop = resources.getDimensionPixelSize(R.dimen.new_tablet_tab_panel_grid_padding_top);
// Lets set double the top padding on the bottom so that the last row shows up properly!
// Your demise, GridView, cannot come fast enough.
final int paddingBottom = paddingTop * 2;
setPadding(padding, paddingTop, padding, paddingBottom);
setOnItemClickListener(new OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
TabsLayoutItemView tab = (TabsLayoutItemView) view;
Tabs.getInstance().selectTab(tab.getTabId());
autoHidePanel();
}
});
}
private class TabsGridLayoutAdapter extends TabsLayoutAdapter {
final private Button.OnClickListener mCloseClickListener;
public TabsGridLayoutAdapter (Context context) {
super(context, R.layout.new_tablet_tabs_item_cell);
mCloseClickListener = new Button.OnClickListener() {
@Override
public void onClick(View v) {
closeTab(v);
}
};
}
@Override
TabsLayoutItemView newView(int position, ViewGroup parent) {
final TabsLayoutItemView item = super.newView(position, parent);
item.setCloseOnClickListener(mCloseClickListener);
return item;
}
@Override
public void bindView(TabsLayoutItemView view, Tab tab) {
super.bindView(view, tab);
// If we're recycling this view, there's a chance it was transformed during
// the close animation. Remove any of those properties.
resetTransforms(view);
}
}
private void populateTabLocations(final Tab removedTab) {
mTabLocations.clear();
final int firstPosition = getFirstVisiblePosition();
final int lastPosition = getLastVisiblePosition();
final int numberOfColumns = getNumColumns();
final int childCount = getChildCount();
final int removedPosition = mTabsAdapter.getPositionForTab(removedTab);
for (int x = 1, i = (removedPosition - firstPosition) + 1; i < childCount; i++, x++) {
final View child = getChildAt(i);
if (child != null) {
mTabLocations.append(x, new PointF(child.getX(), child.getY()));
}
}
final boolean firstChildOffScreen = ((firstPosition > 0) || getChildAt(0).getY() < 0);
final boolean lastChildVisible = (lastPosition - childCount == firstPosition - 1);
final boolean oneItemOnLastRow = (lastPosition % numberOfColumns == 0);
if (firstChildOffScreen && lastChildVisible && oneItemOnLastRow) {
// We need to set the view's bottom padding to prevent a sudden jump as the
// last item in the row is being removed. We then need to remove the padding
// via a sweet animation
final int removedHeight = getChildAt(0).getMeasuredHeight();
final int verticalSpacing =
getResources().getDimensionPixelOffset(R.dimen.new_tablet_tab_panel_grid_vspacing);
ValueAnimator paddingAnimator = ValueAnimator.ofInt(getPaddingBottom() + removedHeight + verticalSpacing, getPaddingBottom());
paddingAnimator.setDuration(ANIM_TIME_MS * 2);
paddingAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
setPadding(getPaddingLeft(), getPaddingTop(), getPaddingRight(), (Integer) animation.getAnimatedValue());
}
});
paddingAnimator.start();
}
}
@Override
public void setTabsPanel(TabsPanel panel) {
mTabsPanel = panel;
}
@Override
public void show() {
setVisibility(View.VISIBLE);
Tabs.getInstance().refreshThumbnails();
Tabs.registerOnTabsChangedListener(this);
refreshTabsData();
}
@Override
public void hide() {
setVisibility(View.GONE);
Tabs.unregisterOnTabsChangedListener(this);
GoannaAppShell.sendEventToGoanna(GoannaEvent.createBroadcastEvent("Tab:Screenshot:Cancel",""));
mTabsAdapter.clear();
}
@Override
public boolean shouldExpand() {
return true;
}
private void autoHidePanel() {
mTabsPanel.autoHidePanel();
}
@Override
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:
if (mTabsAdapter.getCount() > 0) {
animateRemoveTab(tab);
}
final Tabs tabsInstance = Tabs.getInstance();
if (mTabsAdapter.removeTab(tab)) {
if (tab.isPrivate() == mIsPrivate && mTabsAdapter.getCount() > 0) {
int selected = mTabsAdapter.getPositionForTab(tabsInstance.getSelectedTab());
updateSelectedStyle(selected);
}
if(!tab.isPrivate()) {
// Make sure we always have at least one normal tab
final Iterable<Tab> tabs = tabsInstance.getTabsInOrder();
boolean removedTabIsLastNormalTab = true;
for (Tab singleTab : tabs) {
if (!singleTab.isPrivate()) {
removedTabIsLastNormalTab = false;
break;
}
}
if (removedTabIsLastNormalTab) {
tabsInstance.addTab();
}
}
}
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:
case RECORDING_CHANGE:
View view = getChildAt(mTabsAdapter.getPositionForTab(tab) - getFirstVisiblePosition());
if (view == null)
return;
((TabsLayoutItemView) view).assignValues(tab);
break;
}
}
// Updates the selected position in the list so that it will be scrolled to the right place.
private void updateSelectedPosition() {
int selected = mTabsAdapter.getPositionForTab(Tabs.getInstance().getSelectedTab());
updateSelectedStyle(selected);
if (selected != -1) {
setSelection(selected);
}
}
/**
* Updates the selected/unselected style for the tabs.
*
* @param selected position of the selected tab
*/
private void updateSelectedStyle(int selected) {
for (int i = 0; i < mTabsAdapter.getCount(); i++) {
setItemChecked(i, (i == selected));
}
}
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.
ArrayList<Tab> tabData = new ArrayList<>();
Iterable<Tab> allTabs = Tabs.getInstance().getTabsInOrder();
for (Tab tab : allTabs) {
if (tab.isPrivate() == mIsPrivate)
tabData.add(tab);
}
mTabsAdapter.setTabs(tabData);
updateSelectedPosition();
}
private void resetTransforms(View view) {
ViewHelper.setAlpha(view, 1);
ViewHelper.setTranslationX(view, 0);
}
@Override
public void closeAll() {
autoHidePanel();
if (getChildCount() == 0) {
return;
}
final Iterable<Tab> tabs = Tabs.getInstance().getTabsInOrder();
for (Tab tab : tabs) {
// In the normal panel we want to close all tabs (both private and normal),
// but in the private panel we only want to close private tabs.
if (!mIsPrivate || tab.isPrivate()) {
Tabs.getInstance().closeTab(tab, false);
}
}
}
private View getViewForTab(Tab tab) {
final int position = mTabsAdapter.getPositionForTab(tab);
return getChildAt(position - getFirstVisiblePosition());
}
void closeTab(View v) {
TabsLayoutItemView itemView = (TabsLayoutItemView) v.getTag();
Tab tab = Tabs.getInstance().getTab(itemView.getTabId());
Tabs.getInstance().closeTab(tab);
updateSelectedPosition();
}
private void animateRemoveTab(final Tab removedTab) {
final int removedPosition = mTabsAdapter.getPositionForTab(removedTab);
final View removedView = getViewForTab(removedTab);
// The removed position might not have a matching child view
// when it's not within the visible range of positions in the strip.
if (removedView == null) {
return;
}
final int removedHeight = removedView.getMeasuredHeight();
populateTabLocations(removedTab);
getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
@Override
public boolean onPreDraw() {
getViewTreeObserver().removeOnPreDrawListener(this);
// We don't animate the removed child view (it just disappears)
// but we still need its size to animate all affected children
// within the visible viewport.
final int childCount = getChildCount();
final int firstPosition = getFirstVisiblePosition();
final int numberOfColumns = getNumColumns();
final List<Animator> childAnimators = new ArrayList<>();
PropertyValuesHolder translateX, translateY;
for (int x = 0, i = removedPosition - firstPosition ; i < childCount; i++, x++) {
final View child = getChildAt(i);
ObjectAnimator animator;
if (i % numberOfColumns == numberOfColumns - 1) {
// Animate X & Y
translateX = PropertyValuesHolder.ofFloat("translationX", -(mColumnWidth * numberOfColumns), 0);
translateY = PropertyValuesHolder.ofFloat("translationY", removedHeight, 0);
animator = ObjectAnimator.ofPropertyValuesHolder(child, translateX, translateY);
} else {
// Just animate X
translateX = PropertyValuesHolder.ofFloat("translationX", mColumnWidth, 0);
animator = ObjectAnimator.ofPropertyValuesHolder(child, translateX);
}
animator.setStartDelay(x * ANIM_DELAY_MULTIPLE_MS);
childAnimators.add(animator);
}
final AnimatorSet animatorSet = new AnimatorSet();
animatorSet.playTogether(childAnimators);
animatorSet.setDuration(ANIM_TIME_MS);
animatorSet.setInterpolator(ANIM_INTERPOLATOR);
animatorSet.start();
// Set the starting position of the child views - because we are delaying the start
// of the animation, we need to prevent the items being drawn in their final position
// prior to the animation starting
for (int x = 1, i = (removedPosition - firstPosition) + 1; i < childCount; i++, x++) {
final View child = getChildAt(i);
final PointF targetLocation = mTabLocations.get(x+1);
if (targetLocation == null) {
continue;
}
child.setX(targetLocation.x);
child.setY(targetLocation.y);
}
return true;
}
});
}
}