609 lines
21 KiB
Java
609 lines
21 KiB
Java
/*
|
|
* Copyright (C) 2011 The Android Open Source Project
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
package android.support.v7.internal.widget;
|
|
|
|
import android.content.Context;
|
|
import android.content.res.Configuration;
|
|
import android.graphics.drawable.Drawable;
|
|
import android.os.Build;
|
|
import android.support.v4.view.GravityCompat;
|
|
import android.support.v4.view.ViewCompat;
|
|
import android.support.v4.view.ViewPropertyAnimatorCompat;
|
|
import android.support.v4.view.ViewPropertyAnimatorListener;
|
|
import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
|
|
import android.support.v7.app.ActionBar;
|
|
import android.support.v7.appcompat.R;
|
|
import android.support.v7.internal.view.ActionBarPolicy;
|
|
import android.support.v7.widget.LinearLayoutCompat;
|
|
import android.text.TextUtils;
|
|
import android.text.TextUtils.TruncateAt;
|
|
import android.view.Gravity;
|
|
import android.view.View;
|
|
import android.view.ViewGroup;
|
|
import android.view.ViewParent;
|
|
import android.view.accessibility.AccessibilityEvent;
|
|
import android.view.accessibility.AccessibilityNodeInfo;
|
|
import android.view.animation.DecelerateInterpolator;
|
|
import android.view.animation.Interpolator;
|
|
import android.widget.BaseAdapter;
|
|
import android.widget.HorizontalScrollView;
|
|
import android.widget.ImageView;
|
|
import android.widget.LinearLayout;
|
|
import android.widget.ListView;
|
|
import android.widget.TextView;
|
|
import android.widget.Toast;
|
|
|
|
/**
|
|
* This widget implements the dynamic action bar tab behavior that can change across different
|
|
* configurations or circumstances.
|
|
*
|
|
* @hide
|
|
*/
|
|
public class ScrollingTabContainerView extends HorizontalScrollView
|
|
implements AdapterViewCompat.OnItemClickListener {
|
|
|
|
private static final String TAG = "ScrollingTabContainerView";
|
|
Runnable mTabSelector;
|
|
private TabClickListener mTabClickListener;
|
|
|
|
private LinearLayoutCompat mTabLayout;
|
|
private SpinnerCompat mTabSpinner;
|
|
private boolean mAllowCollapse;
|
|
|
|
int mMaxTabWidth;
|
|
int mStackedTabMaxWidth;
|
|
private int mContentHeight;
|
|
private int mSelectedTabIndex;
|
|
|
|
protected ViewPropertyAnimatorCompat mVisibilityAnim;
|
|
protected final VisibilityAnimListener mVisAnimListener = new VisibilityAnimListener();
|
|
|
|
private static final Interpolator sAlphaInterpolator = new DecelerateInterpolator();
|
|
|
|
private static final int FADE_DURATION = 200;
|
|
|
|
public ScrollingTabContainerView(Context context) {
|
|
super(context);
|
|
|
|
setHorizontalScrollBarEnabled(false);
|
|
|
|
ActionBarPolicy abp = ActionBarPolicy.get(context);
|
|
setContentHeight(abp.getTabContainerHeight());
|
|
mStackedTabMaxWidth = abp.getStackedTabMaxWidth();
|
|
|
|
mTabLayout = createTabLayout();
|
|
addView(mTabLayout, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
|
|
ViewGroup.LayoutParams.MATCH_PARENT));
|
|
}
|
|
|
|
@Override
|
|
public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
|
final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
|
|
final boolean lockedExpanded = widthMode == MeasureSpec.EXACTLY;
|
|
setFillViewport(lockedExpanded);
|
|
|
|
final int childCount = mTabLayout.getChildCount();
|
|
if (childCount > 1 &&
|
|
(widthMode == MeasureSpec.EXACTLY || widthMode == MeasureSpec.AT_MOST)) {
|
|
if (childCount > 2) {
|
|
mMaxTabWidth = (int) (MeasureSpec.getSize(widthMeasureSpec) * 0.4f);
|
|
} else {
|
|
mMaxTabWidth = MeasureSpec.getSize(widthMeasureSpec) / 2;
|
|
}
|
|
mMaxTabWidth = Math.min(mMaxTabWidth, mStackedTabMaxWidth);
|
|
} else {
|
|
mMaxTabWidth = -1;
|
|
}
|
|
|
|
heightMeasureSpec = MeasureSpec.makeMeasureSpec(mContentHeight, MeasureSpec.EXACTLY);
|
|
|
|
final boolean canCollapse = !lockedExpanded && mAllowCollapse;
|
|
|
|
if (canCollapse) {
|
|
// See if we should expand
|
|
mTabLayout.measure(MeasureSpec.UNSPECIFIED, heightMeasureSpec);
|
|
if (mTabLayout.getMeasuredWidth() > MeasureSpec.getSize(widthMeasureSpec)) {
|
|
performCollapse();
|
|
} else {
|
|
performExpand();
|
|
}
|
|
} else {
|
|
performExpand();
|
|
}
|
|
|
|
final int oldWidth = getMeasuredWidth();
|
|
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
|
final int newWidth = getMeasuredWidth();
|
|
|
|
if (lockedExpanded && oldWidth != newWidth) {
|
|
// Recenter the tab display if we're at a new (scrollable) size.
|
|
setTabSelected(mSelectedTabIndex);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Indicates whether this view is collapsed into a dropdown menu instead
|
|
* of traditional tabs.
|
|
* @return true if showing as a spinner
|
|
*/
|
|
private boolean isCollapsed() {
|
|
return mTabSpinner != null && mTabSpinner.getParent() == this;
|
|
}
|
|
|
|
public void setAllowCollapse(boolean allowCollapse) {
|
|
mAllowCollapse = allowCollapse;
|
|
}
|
|
|
|
private void performCollapse() {
|
|
if (isCollapsed()) return;
|
|
|
|
if (mTabSpinner == null) {
|
|
mTabSpinner = createSpinner();
|
|
}
|
|
removeView(mTabLayout);
|
|
addView(mTabSpinner, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
|
|
ViewGroup.LayoutParams.MATCH_PARENT));
|
|
if (mTabSpinner.getAdapter() == null) {
|
|
mTabSpinner.setAdapter(new TabAdapter());
|
|
}
|
|
if (mTabSelector != null) {
|
|
removeCallbacks(mTabSelector);
|
|
mTabSelector = null;
|
|
}
|
|
mTabSpinner.setSelection(mSelectedTabIndex);
|
|
}
|
|
|
|
private boolean performExpand() {
|
|
if (!isCollapsed()) return false;
|
|
|
|
removeView(mTabSpinner);
|
|
addView(mTabLayout, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
|
|
ViewGroup.LayoutParams.MATCH_PARENT));
|
|
setTabSelected(mTabSpinner.getSelectedItemPosition());
|
|
return false;
|
|
}
|
|
|
|
public void setTabSelected(int position) {
|
|
mSelectedTabIndex = position;
|
|
final int tabCount = mTabLayout.getChildCount();
|
|
for (int i = 0; i < tabCount; i++) {
|
|
final View child = mTabLayout.getChildAt(i);
|
|
final boolean isSelected = i == position;
|
|
child.setSelected(isSelected);
|
|
if (isSelected) {
|
|
animateToTab(position);
|
|
}
|
|
}
|
|
if (mTabSpinner != null && position >= 0) {
|
|
mTabSpinner.setSelection(position);
|
|
}
|
|
}
|
|
|
|
public void setContentHeight(int contentHeight) {
|
|
mContentHeight = contentHeight;
|
|
requestLayout();
|
|
}
|
|
|
|
private LinearLayoutCompat createTabLayout() {
|
|
final LinearLayoutCompat tabLayout = new LinearLayoutCompat(getContext(), null,
|
|
R.attr.actionBarTabBarStyle);
|
|
tabLayout.setMeasureWithLargestChildEnabled(true);
|
|
tabLayout.setGravity(Gravity.CENTER);
|
|
tabLayout.setLayoutParams(new LinearLayoutCompat.LayoutParams(
|
|
LinearLayoutCompat.LayoutParams.WRAP_CONTENT, LinearLayoutCompat.LayoutParams.MATCH_PARENT));
|
|
return tabLayout;
|
|
}
|
|
|
|
private SpinnerCompat createSpinner() {
|
|
final SpinnerCompat spinner = new SpinnerCompat(getContext(), null,
|
|
R.attr.actionDropDownStyle);
|
|
spinner.setLayoutParams(new LinearLayoutCompat.LayoutParams(
|
|
LinearLayoutCompat.LayoutParams.WRAP_CONTENT, LinearLayoutCompat.LayoutParams.MATCH_PARENT));
|
|
spinner.setOnItemClickListenerInt(this);
|
|
return spinner;
|
|
}
|
|
|
|
protected void onConfigurationChanged(Configuration newConfig) {
|
|
if (Build.VERSION.SDK_INT >= 8) {
|
|
super.onConfigurationChanged(newConfig);
|
|
}
|
|
|
|
ActionBarPolicy abp = ActionBarPolicy.get(getContext());
|
|
// Action bar can change size on configuration changes.
|
|
// Reread the desired height from the theme-specified style.
|
|
setContentHeight(abp.getTabContainerHeight());
|
|
mStackedTabMaxWidth = abp.getStackedTabMaxWidth();
|
|
}
|
|
|
|
public void animateToVisibility(int visibility) {
|
|
if (mVisibilityAnim != null) {
|
|
mVisibilityAnim.cancel();
|
|
}
|
|
if (visibility == VISIBLE) {
|
|
if (getVisibility() != VISIBLE) {
|
|
ViewCompat.setAlpha(this, 0f);
|
|
}
|
|
|
|
ViewPropertyAnimatorCompat anim = ViewCompat.animate(this).alpha(1f);
|
|
anim.setDuration(FADE_DURATION);
|
|
|
|
anim.setInterpolator(sAlphaInterpolator);
|
|
anim.setListener(mVisAnimListener.withFinalVisibility(anim, visibility));
|
|
anim.start();
|
|
} else {
|
|
ViewPropertyAnimatorCompat anim = ViewCompat.animate(this).alpha(0f);
|
|
anim.setDuration(FADE_DURATION);
|
|
|
|
anim.setInterpolator(sAlphaInterpolator);
|
|
anim.setListener(mVisAnimListener.withFinalVisibility(anim, visibility));
|
|
anim.start();
|
|
}
|
|
}
|
|
|
|
public void animateToTab(final int position) {
|
|
final View tabView = mTabLayout.getChildAt(position);
|
|
if (mTabSelector != null) {
|
|
removeCallbacks(mTabSelector);
|
|
}
|
|
mTabSelector = new Runnable() {
|
|
public void run() {
|
|
final int scrollPos = tabView.getLeft() - (getWidth() - tabView.getWidth()) / 2;
|
|
smoothScrollTo(scrollPos, 0);
|
|
mTabSelector = null;
|
|
}
|
|
};
|
|
post(mTabSelector);
|
|
}
|
|
|
|
@Override
|
|
public void onAttachedToWindow() {
|
|
super.onAttachedToWindow();
|
|
if (mTabSelector != null) {
|
|
// Re-post the selector we saved
|
|
post(mTabSelector);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void onDetachedFromWindow() {
|
|
super.onDetachedFromWindow();
|
|
if (mTabSelector != null) {
|
|
removeCallbacks(mTabSelector);
|
|
}
|
|
}
|
|
|
|
private TabView createTabView(ActionBar.Tab tab, boolean forAdapter) {
|
|
final TabView tabView = new TabView(getContext(), tab, forAdapter);
|
|
if (forAdapter) {
|
|
tabView.setBackgroundDrawable(null);
|
|
tabView.setLayoutParams(new ListView.LayoutParams(ListView.LayoutParams.MATCH_PARENT,
|
|
mContentHeight));
|
|
} else {
|
|
tabView.setFocusable(true);
|
|
|
|
if (mTabClickListener == null) {
|
|
mTabClickListener = new TabClickListener();
|
|
}
|
|
tabView.setOnClickListener(mTabClickListener);
|
|
}
|
|
return tabView;
|
|
}
|
|
|
|
public void addTab(ActionBar.Tab tab, boolean setSelected) {
|
|
TabView tabView = createTabView(tab, false);
|
|
mTabLayout.addView(tabView, new LinearLayoutCompat.LayoutParams(0,
|
|
LayoutParams.MATCH_PARENT, 1));
|
|
if (mTabSpinner != null) {
|
|
((TabAdapter) mTabSpinner.getAdapter()).notifyDataSetChanged();
|
|
}
|
|
if (setSelected) {
|
|
tabView.setSelected(true);
|
|
}
|
|
if (mAllowCollapse) {
|
|
requestLayout();
|
|
}
|
|
}
|
|
|
|
public void addTab(ActionBar.Tab tab, int position, boolean setSelected) {
|
|
final TabView tabView = createTabView(tab, false);
|
|
mTabLayout.addView(tabView, position, new LinearLayoutCompat.LayoutParams(
|
|
0, LayoutParams.MATCH_PARENT, 1));
|
|
if (mTabSpinner != null) {
|
|
((TabAdapter) mTabSpinner.getAdapter()).notifyDataSetChanged();
|
|
}
|
|
if (setSelected) {
|
|
tabView.setSelected(true);
|
|
}
|
|
if (mAllowCollapse) {
|
|
requestLayout();
|
|
}
|
|
}
|
|
|
|
public void updateTab(int position) {
|
|
((TabView) mTabLayout.getChildAt(position)).update();
|
|
if (mTabSpinner != null) {
|
|
((TabAdapter) mTabSpinner.getAdapter()).notifyDataSetChanged();
|
|
}
|
|
if (mAllowCollapse) {
|
|
requestLayout();
|
|
}
|
|
}
|
|
|
|
public void removeTabAt(int position) {
|
|
mTabLayout.removeViewAt(position);
|
|
if (mTabSpinner != null) {
|
|
((TabAdapter) mTabSpinner.getAdapter()).notifyDataSetChanged();
|
|
}
|
|
if (mAllowCollapse) {
|
|
requestLayout();
|
|
}
|
|
}
|
|
|
|
public void removeAllTabs() {
|
|
mTabLayout.removeAllViews();
|
|
if (mTabSpinner != null) {
|
|
((TabAdapter) mTabSpinner.getAdapter()).notifyDataSetChanged();
|
|
}
|
|
if (mAllowCollapse) {
|
|
requestLayout();
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void onItemClick(AdapterViewCompat<?> parent, View view, int position, long id) {
|
|
TabView tabView = (TabView) view;
|
|
tabView.getTab().select();
|
|
}
|
|
|
|
private class TabView extends LinearLayoutCompat implements OnLongClickListener {
|
|
private final int[] BG_ATTRS = {
|
|
android.R.attr.background
|
|
};
|
|
|
|
private ActionBar.Tab mTab;
|
|
private TextView mTextView;
|
|
private ImageView mIconView;
|
|
private View mCustomView;
|
|
|
|
public TabView(Context context, ActionBar.Tab tab, boolean forList) {
|
|
super(context, null, R.attr.actionBarTabStyle);
|
|
mTab = tab;
|
|
|
|
TintTypedArray a = TintTypedArray.obtainStyledAttributes(context, null, BG_ATTRS,
|
|
R.attr.actionBarTabStyle, 0);
|
|
if (a.hasValue(0)) {
|
|
setBackgroundDrawable(a.getDrawable(0));
|
|
}
|
|
a.recycle();
|
|
|
|
if (forList) {
|
|
setGravity(GravityCompat.START | Gravity.CENTER_VERTICAL);
|
|
}
|
|
|
|
update();
|
|
}
|
|
|
|
public void bindTab(ActionBar.Tab tab) {
|
|
mTab = tab;
|
|
update();
|
|
}
|
|
|
|
@Override
|
|
public void setSelected(boolean selected) {
|
|
final boolean changed = (isSelected() != selected);
|
|
super.setSelected(selected);
|
|
if (changed && selected) {
|
|
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
|
|
super.onInitializeAccessibilityEvent(event);
|
|
// This view masquerades as an action bar tab.
|
|
event.setClassName(ActionBar.Tab.class.getName());
|
|
}
|
|
|
|
@Override
|
|
public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
|
|
super.onInitializeAccessibilityNodeInfo(info);
|
|
|
|
if (Build.VERSION.SDK_INT >= 14) {
|
|
// This view masquerades as an action bar tab.
|
|
info.setClassName(ActionBar.Tab.class.getName());
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
|
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
|
|
|
// Re-measure if we went beyond our maximum size.
|
|
if (mMaxTabWidth > 0 && getMeasuredWidth() > mMaxTabWidth) {
|
|
super.onMeasure(MeasureSpec.makeMeasureSpec(mMaxTabWidth, MeasureSpec.EXACTLY),
|
|
heightMeasureSpec);
|
|
}
|
|
}
|
|
|
|
public void update() {
|
|
final ActionBar.Tab tab = mTab;
|
|
final View custom = tab.getCustomView();
|
|
if (custom != null) {
|
|
final ViewParent customParent = custom.getParent();
|
|
if (customParent != this) {
|
|
if (customParent != null) ((ViewGroup) customParent).removeView(custom);
|
|
addView(custom);
|
|
}
|
|
mCustomView = custom;
|
|
if (mTextView != null) mTextView.setVisibility(GONE);
|
|
if (mIconView != null) {
|
|
mIconView.setVisibility(GONE);
|
|
mIconView.setImageDrawable(null);
|
|
}
|
|
} else {
|
|
if (mCustomView != null) {
|
|
removeView(mCustomView);
|
|
mCustomView = null;
|
|
}
|
|
|
|
final Drawable icon = tab.getIcon();
|
|
final CharSequence text = tab.getText();
|
|
|
|
if (icon != null) {
|
|
if (mIconView == null) {
|
|
ImageView iconView = new ImageView(getContext());
|
|
LayoutParams lp = new LayoutParams(LayoutParams.WRAP_CONTENT,
|
|
LayoutParams.WRAP_CONTENT);
|
|
lp.gravity = Gravity.CENTER_VERTICAL;
|
|
iconView.setLayoutParams(lp);
|
|
addView(iconView, 0);
|
|
mIconView = iconView;
|
|
}
|
|
mIconView.setImageDrawable(icon);
|
|
mIconView.setVisibility(VISIBLE);
|
|
} else if (mIconView != null) {
|
|
mIconView.setVisibility(GONE);
|
|
mIconView.setImageDrawable(null);
|
|
}
|
|
|
|
final boolean hasText = !TextUtils.isEmpty(text);
|
|
if (hasText) {
|
|
if (mTextView == null) {
|
|
TextView textView = new CompatTextView(getContext(), null,
|
|
R.attr.actionBarTabTextStyle);
|
|
textView.setEllipsize(TruncateAt.END);
|
|
LayoutParams lp = new LayoutParams(LayoutParams.WRAP_CONTENT,
|
|
LayoutParams.WRAP_CONTENT);
|
|
lp.gravity = Gravity.CENTER_VERTICAL;
|
|
textView.setLayoutParams(lp);
|
|
addView(textView);
|
|
mTextView = textView;
|
|
}
|
|
mTextView.setText(text);
|
|
mTextView.setVisibility(VISIBLE);
|
|
} else if (mTextView != null) {
|
|
mTextView.setVisibility(GONE);
|
|
mTextView.setText(null);
|
|
}
|
|
|
|
if (mIconView != null) {
|
|
mIconView.setContentDescription(tab.getContentDescription());
|
|
}
|
|
|
|
if (!hasText && !TextUtils.isEmpty(tab.getContentDescription())) {
|
|
setOnLongClickListener(this);
|
|
} else {
|
|
setOnLongClickListener(null);
|
|
setLongClickable(false);
|
|
}
|
|
}
|
|
}
|
|
|
|
public boolean onLongClick(View v) {
|
|
final int[] screenPos = new int[2];
|
|
getLocationOnScreen(screenPos);
|
|
|
|
final Context context = getContext();
|
|
final int width = getWidth();
|
|
final int height = getHeight();
|
|
final int screenWidth = context.getResources().getDisplayMetrics().widthPixels;
|
|
|
|
Toast cheatSheet = Toast.makeText(context, mTab.getContentDescription(),
|
|
Toast.LENGTH_SHORT);
|
|
// Show under the tab
|
|
cheatSheet.setGravity(Gravity.TOP | Gravity.CENTER_HORIZONTAL,
|
|
(screenPos[0] + width / 2) - screenWidth / 2, height);
|
|
|
|
cheatSheet.show();
|
|
return true;
|
|
}
|
|
|
|
public ActionBar.Tab getTab() {
|
|
return mTab;
|
|
}
|
|
}
|
|
|
|
private class TabAdapter extends BaseAdapter {
|
|
@Override
|
|
public int getCount() {
|
|
return mTabLayout.getChildCount();
|
|
}
|
|
|
|
@Override
|
|
public Object getItem(int position) {
|
|
return ((TabView) mTabLayout.getChildAt(position)).getTab();
|
|
}
|
|
|
|
@Override
|
|
public long getItemId(int position) {
|
|
return position;
|
|
}
|
|
|
|
@Override
|
|
public View getView(int position, View convertView, ViewGroup parent) {
|
|
if (convertView == null) {
|
|
convertView = createTabView((ActionBar.Tab) getItem(position), true);
|
|
} else {
|
|
((TabView) convertView).bindTab((ActionBar.Tab) getItem(position));
|
|
}
|
|
return convertView;
|
|
}
|
|
}
|
|
|
|
private class TabClickListener implements OnClickListener {
|
|
public void onClick(View view) {
|
|
TabView tabView = (TabView) view;
|
|
tabView.getTab().select();
|
|
final int tabCount = mTabLayout.getChildCount();
|
|
for (int i = 0; i < tabCount; i++) {
|
|
final View child = mTabLayout.getChildAt(i);
|
|
child.setSelected(child == view);
|
|
}
|
|
}
|
|
}
|
|
|
|
protected class VisibilityAnimListener implements ViewPropertyAnimatorListener {
|
|
private boolean mCanceled = false;
|
|
private int mFinalVisibility;
|
|
|
|
public VisibilityAnimListener withFinalVisibility(ViewPropertyAnimatorCompat animation,
|
|
int visibility) {
|
|
mFinalVisibility = visibility;
|
|
mVisibilityAnim = animation;
|
|
return this;
|
|
}
|
|
|
|
@Override
|
|
public void onAnimationStart(View view) {
|
|
setVisibility(VISIBLE);
|
|
mCanceled = false;
|
|
}
|
|
|
|
@Override
|
|
public void onAnimationEnd(View view) {
|
|
if (mCanceled) return;
|
|
|
|
mVisibilityAnim = null;
|
|
setVisibility(mFinalVisibility);
|
|
}
|
|
|
|
@Override
|
|
public void onAnimationCancel(View view) {
|
|
mCanceled = true;
|
|
}
|
|
}
|
|
}
|
|
|