/* * Copyright (C) 2005-2009 Team XBMC * http://xbmc.org * * This Program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2, or (at your option) * any later version. * * This Program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with XBMC Remote; see the file license. If not, write to * the Free Software Foundation, 675 Mass Ave, Cambridge, MA 02139, USA. * http://www.gnu.org/copyleft/gpl.html * */ package org.xbmc.android.widget.slidingtabs; import org.xbmc.android.remote.R; import org.xbmc.android.widget.slidingtabs.SlidingTabHost.SlidingTabSpec; import android.content.Context; import android.content.res.TypedArray; import android.util.AttributeSet; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.widget.FrameLayout; import android.widget.ImageButton; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.TabHost; import android.widget.TextView; /** * Displays the sliding tabs. * * Displays a list of icons representing each page in the parent's tab * collection. The container object for this widget is * {@link org.xbmc.android.widget.slidingtabs.SlidingTabHost SlidingTabHost}. * When the user selects a tab, this object sends a message to the parent * container, SlidingTabHost, to tell it to switch the displayed page. You * typically won't use many methods directly on this object. The container * SlidingTabHost is used to add labels, add the callback handler, and manage * callbacks. * * @author Team XBMC */ public class SlidingTabWidget extends LinearLayout { private final static int SCROLLBAR_HEIGHT = 42; private final static int SHADOW_PADDING = 18; private int mSeparationWidth; protected Context mContext; private OnTabSelectionChanged mSelectionChangedListener; private int mSelectedTab = 0; private final LinearLayout mOuterLayout; private final ImageButton mSlider; private final LinearLayout mInverseSliderBackground; private final LinearLayout mOverlayLayout; private final ImageView mOverlayIcon; private final TextView mOverlayText; private final SnapAnimation mSnapAnimation; private FrameLayout mTabContent; private int mNumTabs = 0; private int mInverseSliderWidth = 0; private int mSliderMoveWidth = 0; private int mBackMoveWidth = 0; float mBackMoveFactor = 0; private static float sScale; public SlidingTabWidget(Context context) { this(context, null); mContext = context; } public SlidingTabWidget(Context context, AttributeSet attrs) { this(context, attrs, R.attr.slidingTabWidgetStyle); mContext = context; } public SlidingTabWidget(Context context, AttributeSet attrs, int defStyle) { super(context, attrs); sScale = context.getResources().getDisplayMetrics().density; mContext = context; setOrientation(LinearLayout.HORIZONTAL); // inflate widget layout from xml inflate(context, R.layout.slidingtab_widget, this); // set all the view references mOuterLayout = (LinearLayout)findViewById(R.id.SlidingTabLinearLayoutOuter); mSlider = (ImageButton)findViewById(R.id.SlidingTabImageButtonSlider); mInverseSliderBackground = (LinearLayout)findViewById(R.id.SlidingTabLinearLayoutBackslider); mSnapAnimation = new SnapAnimation(mSlider, mInverseSliderBackground); mSlider.setOnTouchListener(new SliderOnTouchListener()); // inflate and prepare the overlay final LayoutInflater inflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); mOverlayLayout = (LinearLayout) inflater.inflate(R.layout.slidingtab_overlay, mTabContent, false); mOverlayIcon = (ImageView)mOverlayLayout.findViewById(R.id.slidingtab_overlay_image); mOverlayText = (TextView)mOverlayLayout.findViewById(R.id.slidingtab_overlay_label); // Deal with focus, as we don't want the focus to go by default // to a tab other than the current tab setFocusable(true); // setOnFocusChangeListener(this); setOnKeyListener(new OnKeyListener() { public boolean onKey(View v, int keyCode, KeyEvent event) { if (event.getAction() == KeyEvent.ACTION_DOWN) { switch (keyCode) { case KeyEvent.KEYCODE_DPAD_LEFT: if (mSelectedTab > 0) { snapTo(mSelectedTab - 1); setCurrentTab(mSelectedTab - 1); } return true; case KeyEvent.KEYCODE_DPAD_RIGHT: if (mSelectedTab < mNumTabs + 1) { snapTo(mSelectedTab + 1); setCurrentTab(mSelectedTab + 1); } return true; } } else { return true; } mTabContent.requestFocus(View.FOCUS_FORWARD); return mTabContent.dispatchKeyEvent(event); } }); // apply the styled attributes TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SlidingTabWidget, defStyle, 0); mSeparationWidth = a.getInt(R.styleable.SlidingTabWidget_separationWidth, 0); if (mSeparationWidth == 0) { mSeparationWidth = (int)(75 * sScale); } a.recycle(); } /** * Adds a new tab and sets the correct paddings defined by the * separation width. * @param newTab */ void addTab(SlidingTabSpec newTab) { BackgroundImage bgImg = new BackgroundImage(mContext, mNumTabs, newTab); final int drawableWidth = bgImg.getDrawable().getIntrinsicWidth(); final int drawableHeight = bgImg.getDrawable().getIntrinsicHeight(); final int hPadding = Math.round((float)(mSeparationWidth - drawableWidth) / 2.0f); final int vPadding = Math.round((float)((int)(SCROLLBAR_HEIGHT * sScale) - drawableHeight) / 2.0f); bgImg.setOverlayIconResource(newTab.getBigIconResource()); final int sliderWidth = mSlider.getBackground().getIntrinsicWidth() - 2 * SHADOW_PADDING; final int borderVPadding = (int) Math.floor((float)(sliderWidth - drawableWidth) / 2.0f); if (mNumTabs == 0) { bgImg.setPadding(borderVPadding, vPadding, hPadding, 0); mSlider.setImageResource(newTab.getActiveIconResource()); mInverseSliderWidth += (borderVPadding + drawableWidth + hPadding); } else { if (mNumTabs > 1) { BackgroundImage lastTab = (BackgroundImage)mInverseSliderBackground.getChildAt(mNumTabs - 1); mInverseSliderWidth += (lastTab.getPaddingLeft() - lastTab.getPaddingRight()); lastTab.setPadding(lastTab.getPaddingLeft(), lastTab.getPaddingTop(), lastTab.getPaddingLeft(), 0); } mInverseSliderWidth += (hPadding + drawableWidth + borderVPadding); bgImg.setPadding(hPadding, vPadding, borderVPadding, 0); } if (bgImg.getLayoutParams() == null) { final LinearLayout.LayoutParams lp = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, 1.0f); lp.setMargins(0, 0, 0, 0); bgImg.setLayoutParams(lp); } // ensure you can navigate to the tab with the keyboard, and you can touch it bgImg.setFocusable(false); bgImg.setClickable(true); // make the buttons accessible by click directly. bgImg.setOnClickListener(new OnClickListener() { public void onClick(View v) { updateLayoutDimensions(); final BackgroundImage bgImg = (BackgroundImage)v; if (bgImg.getTabIndex() != mSelectedTab) { mSlider.setImageResource(bgImg.getSliderIconResource()); snapTo(bgImg.getTabIndex()); setCurrentTab(bgImg.getTabIndex()); } } }); mInverseSliderBackground.addView(bgImg); mNumTabs++; /* StringBuilder sb = new StringBuilder(); int total = 0; for (int i = 0; i < mNumTabs; i++) { BackgroundImage img = (BackgroundImage)mInverseSliderBackground.getChildAt(i); total += img.getPaddingLeft(); total += img.getDrawable().getIntrinsicWidth(); total += img.getPaddingRight(); sb.append("[" + img.getPaddingLeft() + "|" + img.getDrawable().getIntrinsicWidth() + "|" + img.getPaddingRight() + "] "); } mInverseSliderBackground.invalidate(); Log.i("padding", total + ": " + sb.toString() + "(" + mInverseSliderWidth + ")"); */ } /** * Automatically animates the slider (and background) to given tabindex. * @param tabIndex */ void moveTo(int tabIndex) { updateLayoutDimensions(); final BackgroundImage bgImg = (BackgroundImage)getChildTabViewAt(tabIndex); if (bgImg != null && tabIndex != mSelectedTab) { mSlider.setImageResource(bgImg.getSliderIconResource()); snapTo(bgImg.getTabIndex()); setCurrentTab(bgImg.getTabIndex()); } } /** * Moves the slider to a position * @param pos Absolute position of outer-left edge of the slider (0 = left aligned) */ private void moveSlider(int pos) { LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT); params.setMargins(pos, 0, 0, 0); mSlider.setLayoutParams(params); } /** * Moves the background to a position. Won't move if all icons already visible. * @param pos Relative position of all background icons. */ private void moveBackground(int pos) { // only move background if there are more icons than visible if (pos != 0 && mBackMoveFactor > 0) { mInverseSliderBackground.offsetLeftAndRight(pos); mInverseSliderBackground.invalidate(); } } /** * Moves the slider (and the background) to the correct position. Note that * the actual content switch is not done in here and must be manually called: * <pre>mSelectionChangedListener.onTabSelectionChanged(mCurrTabIndex, true);</pre> * @param tabIndex Tab index (first tab = 0) */ private void snapTo(int tabIndex) { if (mBackMoveFactor > 0) { // background scrolls final float snapWidth; if (mNumTabs > 1) { snapWidth = (float)mSliderMoveWidth / (float)(mNumTabs - 1); } else { snapWidth = mSliderMoveWidth; } final int snapPos = Math.round(tabIndex * snapWidth); final int backSnapPos = -(int)Math.round((snapPos * mBackMoveFactor)); // Log.i("snap", "Snapping to " + index + "(" + snapPos + " / " + backSnapPos + ")"); mSnapAnimation.init(snapPos, backSnapPos); } else { // background doesn't scroll final BackgroundImage bgImg = (BackgroundImage)mInverseSliderBackground.getChildAt(tabIndex); final int snapPos = bgImg.getCenteredPosition() - Math.round((float)mSlider.getBackground().getIntrinsicWidth() / 2.0f) + SHADOW_PADDING; mSnapAnimation.init(snapPos, SnapAnimation.NO_ANIMATION); } mSnapAnimation.setDuration(100); mSlider.startAnimation(mSnapAnimation); } /** * Updates layout dimensions. This should be done only once, though I * haven't found out yet how. Should be called when the layout is rendered. */ private void updateLayoutDimensions() { if (mSliderMoveWidth == 0) { mSliderMoveWidth = mOuterLayout.getWidth() - mSlider.getBackground().getIntrinsicWidth() + (2 * SHADOW_PADDING); } if (mBackMoveWidth == 0) { mBackMoveWidth = mInverseSliderWidth - mOuterLayout.getWidth() + mOuterLayout.getPaddingRight() + mOuterLayout.getPaddingLeft();// - (2 * SHADOW_PADDING); } if (mBackMoveFactor == 0) { mBackMoveFactor = (float)mBackMoveWidth / (float)mSliderMoveWidth; } } /** * Returns background ImageButton at the given index. * @param index The zero-based index of the tab indicator view to return * @return the tab indicator view at the given index */ public View getChildTabViewAt(int index) { return mInverseSliderBackground.getChildAt(index); } /** * Sets the reference to the tab content layer (needed for the overlay). * @param layout */ void setTabContent(FrameLayout layout) { mTabContent = layout; } /** * Returns the number of tab indicator views. * @return the number of tab indicator views. */ public int getTabCount() { return mInverseSliderBackground.getChildCount(); } /** * Sets the current tab. * * Note that it doesn't move the slider, this must be done separately. * However, it does switch the content. * * @param tabIndex Which tab is going to be selected */ public void setCurrentTab(int tabIndex) { if (tabIndex < 0 || tabIndex >= getTabCount()) { return; } mSelectionChangedListener.onTabSelectionChanged(tabIndex, true); mSelectedTab = tabIndex; } /** * Sets the current tab and focuses the UI on it. This method makes sure * that the focused tab matches the selected tab, normally at * {@link #setCurrentTab}. Normally this would not be an issue if we go * through the UI, since the UI is responsible for calling * TabWidget.onFocusChanged(), but in the case where we are selecting the * tab programmatically, we'll need to make sure focus keeps up. * * @param index * The tab that you want focused (highlighted in orange) and * selected (tab brought to the front of the widget) * * @see #setCurrentTab */ public void focusCurrentTab2(int index) { final int oldTab = mSelectedTab; // set the tab setCurrentTab(index); // change the focus if applicable. if (oldTab != index) { // getChildTabViewAt(index).requestFocus(); } } /* @Override public void setEnabled(boolean enabled) { super.setEnabled(enabled); int count = getTabCount(); for (int i = 0; i < count; i++) { View child = getChildTabViewAt(i); child.setEnabled(enabled); } }*/ /** * Takes care of all the sliding stuff. */ private class SliderOnTouchListener implements View.OnTouchListener { private int mClickPos = 0; private int mCurrTabIndex = -1; private boolean mIsMoving = false; public boolean onTouch(View v, MotionEvent event) { updateLayoutDimensions(); final int sliderRelPos = (int)event.getX() - mClickPos; final int sliderAbsPos = mSlider.getLeft() + sliderRelPos; final int bgAbsPos = -(int)Math.round(((sliderAbsPos) * mBackMoveFactor)); final int bgRelPos = bgAbsPos - mInverseSliderBackground.getLeft(); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: mClickPos = (int)event.getX(); break; case MotionEvent.ACTION_UP: if (mIsMoving) { // remove overlay mTabContent.removeView(mOverlayLayout); // snap to selected position and change the tab contents snapTo(mCurrTabIndex); setCurrentTab(mCurrTabIndex); mIsMoving = false; } break; case MotionEvent.ACTION_MOVE: if (!mIsMoving) { // add overlay mTabContent.addView(mOverlayLayout); } mIsMoving = true; // only move within boundaries if (sliderAbsPos >= mOuterLayout.getLeft() + mOuterLayout.getPaddingLeft() && sliderAbsPos <= mOuterLayout.getWidth() - mOuterLayout.getPaddingRight() - mSlider.getWidth() + (2 * SHADOW_PADDING)) { moveSlider(sliderAbsPos); moveBackground(bgRelPos); // find out nearest background icon final int slPos = mSlider.getLeft() + (int)Math.round((float)mSlider.getWidth() / 2.0f) - SHADOW_PADDING; int minVal = -1; BackgroundImage nearestTab = null; for (int i = 0; i < mNumTabs; i++) { // TODO some basic optimization wouldn't hurt. final BackgroundImage bgTab = (BackgroundImage)mInverseSliderBackground.getChildAt(i); final int bgPos = mInverseSliderBackground.getLeft() + bgTab.getCenteredPosition(); if (Math.abs(bgPos - slPos) < minVal || minVal == -1) { minVal = Math.abs(bgPos - slPos); nearestTab = bgTab; } } // update overlay icons, text and slider icon on changes if (mCurrTabIndex == -1 || mCurrTabIndex != nearestTab.getTabIndex()) { mSlider.setImageResource(nearestTab.getSliderIconResource()); mOverlayText.setText(nearestTab.getLabel()); mOverlayIcon.setImageResource(nearestTab.getOverlayIconResource()); mCurrTabIndex = nearestTab.getTabIndex(); } } break; } return true; } } /** * Used for the background icons. They additionally hold some resource * references for direct access. */ private class BackgroundImage extends ImageButton { private final int mSliderIconResource; private final int mTabIndex; private final String mLabel; private int mOverlayIconResource; public BackgroundImage(Context context, int tabIndex, SlidingTabSpec specs) { super(context); mTabIndex = tabIndex; mSliderIconResource = specs.getActiveIconResource(); mLabel = specs.getLabel(); setImageResource(specs.getInactiveIconResource()); setBackgroundDrawable(null); } public void setOverlayIconResource(int res) { mOverlayIconResource = res; } public int getOverlayIconResource() { return mOverlayIconResource; } public int getSliderIconResource() { return mSliderIconResource; } public int getTabIndex() { return mTabIndex; } public String getLabel() { return mLabel; } public int getCenteredPosition() { return getLeft() + getPaddingLeft() + Math.round((float)getDrawable().getIntrinsicWidth() / 2.0f); } } /** * Provides a way for {@link SlidingTabHost} to be notified that the user clicked * on a tab indicator. */ void setTabSelectionListener(OnTabSelectionChanged listener) { mSelectionChangedListener = listener; } public void onFocusChange2(View v, boolean hasFocus) { if (v == this && hasFocus) { getChildTabViewAt(mSelectedTab).requestFocus(); return; } if (hasFocus) { int i = 0; int numTabs = getTabCount(); while (i < numTabs) { if (getChildTabViewAt(i) == v) { setCurrentTab(i); mSelectionChangedListener.onTabSelectionChanged(i, false); break; } i++; } } } /** * Let {@link TabHost} know that the user clicked on a tab indicator. */ static interface OnTabSelectionChanged { /** * Informs the TabHost which tab was selected. It also indicates if the * tab was clicked/pressed or just focused into. * * @param tabIndex * index of the tab that was selected * @param clicked * whether the selection changed due to a touch/click or due * to focus entering the tab through navigation. Pass true if * it was due to a press/click and false otherwise. */ void onTabSelectionChanged(int tabIndex, boolean clicked); } }