/* * 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.v7.app.ActionBar; import android.support.v7.appcompat.R; import android.support.v7.internal.view.ActionBarPolicy; import android.support.v7.widget.AppCompatSpinner; import android.support.v7.widget.AppCompatTextView; 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.AdapterView; import android.widget.BaseAdapter; import android.widget.HorizontalScrollView; import android.widget.ImageView; import android.widget.ListView; import android.widget.Spinner; 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 AdapterView.OnItemSelectedListener { private static final String TAG = "ScrollingTabContainerView"; Runnable mTabSelector; private TabClickListener mTabClickListener; private LinearLayoutCompat mTabLayout; private Spinner 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 Spinner createSpinner() { final Spinner spinner = new AppCompatSpinner(getContext(), null, R.attr.actionDropDownStyle); spinner.setLayoutParams(new LinearLayoutCompat.LayoutParams( LinearLayoutCompat.LayoutParams.WRAP_CONTENT, LinearLayoutCompat.LayoutParams.MATCH_PARENT)); spinner.setOnItemSelectedListener(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 onItemSelected(AdapterView adapterView, View view, int position, long id) { TabView tabView = (TabView) view; tabView.getTab().select(); } @Override public void onNothingSelected(AdapterView adapterView) { // no-op } 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 AppCompatTextView(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; } } }