/*
* Copyright (C) 2013 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 com.android.tabcarousel;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnTouchListener;
import android.view.animation.AnimationUtils;
import android.view.animation.Interpolator;
import android.widget.HorizontalScrollView;
import android.widget.ImageView;
import android.widget.TextView;
import com.actionbarsherlock.R;
import com.nineoldandroids.animation.Animator;
import com.nineoldandroids.animation.Animator.AnimatorListener;
import com.nineoldandroids.animation.ObjectAnimator;
import java.lang.ref.WeakReference;
/**
* This is a horizontally scrolling carousel with 2 tabs.
*/
public class CarouselContainer extends HorizontalScrollView implements OnTouchListener {
/**
* Number of tabs
*/
private static final int TAB_COUNT = 2;
/**
* First tab index
*/
public static final int TAB_INDEX_FIRST = 0;
/**
* Second tab index
*/
public static final int TAB_INDEX_SECOND = 1;
/**
* Y coordinate of the tab at the given index was selected
*/
private static final float[] Y_COORDINATE = new float[TAB_COUNT];
/**
* Alpha layer to be set on the lable view
*/
private static final float MAX_ALPHA = 0.6f;
/**
* Tab width as defined as a fraction of the screen width
*/
private final float mTabWidthScreenFraction;
/**
* Tab height as defined as a fraction of the screen width
*/
private final float mTabHeightScreenFraction;
/**
* Height of the tab label
*/
private final int mTabDisplayLabelHeight;
/**
* Height of the shadow under the tab carousel
*/
private final int mTabShadowHeight;
/**
* Used to determine is the carousel is animating
*/
private boolean mTabCarouselIsAnimating;
/**
* Indicates that both tabs are to be used if true, false if only one
*/
private boolean mDualTabs = true;
/**
* Interface invoked when the user interacts with the carousel
*/
private OnCarouselListener mCarouselListener;
/**
* The first tab in the carousel
*/
private CarouselTab mFirstTab;
/**
* The second tab in the carousel
*/
private CarouselTab mSecondTab;
/**
* Allowed horizontal scroll length
*/
private int mAllowedHorizontalScrollLength = Integer.MIN_VALUE;
/**
* Allowed vertical scroll length
*/
private int mAllowedVerticalScrollLength = Integer.MIN_VALUE;
/**
* The last scrolled position
*/
private int mLastScrollPosition = Integer.MIN_VALUE;
/**
* Current tab index
*/
private int mCurrentTab = TAB_INDEX_FIRST;
/**
* Factor to scale scroll-amount sent to {@code #mCarouselListener}
*/
private float mScrollScaleFactor = 1.0f;
/**
* True to scroll to the pager's current position, false otherwise
*/
private boolean mScrollToCurrentTab = false;
/**
* @param context The {@link Context} to use
* @param attrs The attributes of the XML tag that is inflating the view
*/
public CarouselContainer(Context context, AttributeSet attrs) {
super(context, attrs);
// Add the onTouchListener
setOnTouchListener(this);
// Retrieve the carousel dimensions
final Resources res = getResources();
// Width of the tab
mTabWidthScreenFraction = res.getFraction(R.fraction.tab_width_screen_percentage, 1, 1);
// Height of the tab
mTabHeightScreenFraction = res.getFraction(R.fraction.tab_height_screen_percentage, 1, 1);
// Height of the label
mTabDisplayLabelHeight = res.getDimensionPixelSize(R.dimen.carousel_label_height);
// Height of the image shadow
mTabShadowHeight = res.getDimensionPixelSize(R.dimen.carousel_image_shadow_height);
}
/**
* {@inheritDoc}
*/
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mFirstTab = (CarouselTab) findViewById(R.id.carousel_tab_one);
mFirstTab.setOverlayOnClickListener(new TabClickListener(this, TAB_INDEX_FIRST));
mSecondTab = (CarouselTab) findViewById(R.id.carousel_tab_two);
mSecondTab.setOverlayOnClickListener(new TabClickListener(this, TAB_INDEX_SECOND));
mSecondTab.setAlphaLayerValue(MAX_ALPHA);
}
/**
* {@inheritDoc}
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
final int screenWidth = MeasureSpec.getSize(widthMeasureSpec);
// Compute the width of a tab as a fraction of the screen width
final int tabWidth = Math.round(mTabWidthScreenFraction * screenWidth);
// Find the allowed scrolling length by subtracting the current visible
// screen width
// from the total length of the tabs.
mAllowedHorizontalScrollLength = tabWidth * TAB_COUNT - screenWidth;
// Scrolling by mAllowedHorizontalScrollLength causes listeners to
// scroll by the entire screen amount; compute the scale-factor
// necessary to make this so.
if (mAllowedHorizontalScrollLength == 0) {
// Guard against divide-by-zero.
// This hard-coded value prevents a crash, but won't result in the
// desired scrolling behavior. We rely on the framework calling
// onMeasure()
// again with a non-zero screen width.
mScrollScaleFactor = 1.0f;
} else {
mScrollScaleFactor = screenWidth / mAllowedHorizontalScrollLength;
}
final int tabHeight = Math.round(screenWidth * mTabHeightScreenFraction) + mTabShadowHeight;
// Set the child layout's to be TAB_COUNT * the computed tab
// width so that the layout's children (which are the tabs) will evenly
// split that width.
if (getChildCount() > 0) {
final View child = getChildAt(0);
// Add 1 dip of separation between the tabs
final int seperatorPixels = (int) (TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, 1, getResources().getDisplayMetrics()) + 0.5f);
if (mDualTabs) {
final int size = TAB_COUNT * tabWidth + (TAB_COUNT - 1) * seperatorPixels;
child.measure(measureExact(size), measureExact(tabHeight));
} else {
child.measure(measureExact(screenWidth), measureExact(tabHeight));
}
}
mAllowedVerticalScrollLength = tabHeight - mTabDisplayLabelHeight - mTabShadowHeight;
setMeasuredDimension(resolveSize(screenWidth, widthMeasureSpec),
resolveSize(tabHeight, heightMeasureSpec));
}
/**
* {@inheritDoc}
*/
@SuppressLint("DrawAllocation")
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
if (!mScrollToCurrentTab) {
return;
}
mScrollToCurrentTab = false;
Utils.doAfterLayout(this, new Runnable() {
@Override
public void run() {
scrollTo(mCurrentTab == TAB_INDEX_FIRST ? 0 : mAllowedHorizontalScrollLength, 0);
updateAlphaLayers();
}
});
}
/**
* {@inheritDoc}
*/
@Override
protected void onScrollChanged(int x, int y, int oldX, int oldY) {
super.onScrollChanged(x, y, oldX, oldY);
// Guard against framework issue where onScrollChanged() is called twice
// for each touch-move event. This wreaked havoc on the tab-carousel:
// the
// view-pager moved twice as fast as it should because we called
// fakeDragBy()
// twice with the same value.
if (mLastScrollPosition == x) {
return;
}
// Since we never completely scroll the about/updates tabs off-screen,
// the draggable range is less than the width of the carousel. Our
// listeners don't care about this... if we scroll 75% percent of our
// draggable range, they want to scroll 75% of the entire carousel
// width, not the same number of pixels that we scrolled.
final int scaledL = (int) (x * mScrollScaleFactor);
final int oldScaledL = (int) (oldX * mScrollScaleFactor);
mCarouselListener.onCarouselScrollChanged(scaledL, y, oldScaledL, oldY);
mLastScrollPosition = x;
updateAlphaLayers();
}
/**
* {@inheritDoc}
*/
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
final boolean interceptTouch = super.onInterceptTouchEvent(ev);
if (interceptTouch) {
mCarouselListener.onTouchDown();
}
return interceptTouch;
}
/**
* {@inheritDoc}
*/
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mCarouselListener.onTouchDown();
return true;
case MotionEvent.ACTION_UP:
mCarouselListener.onTouchUp();
return true;
}
return super.onTouchEvent(event);
}
/**
* @return True if the carousel is currently animating, false otherwise
*/
public boolean isTabCarouselIsAnimating() {
return mTabCarouselIsAnimating;
}
/**
* Reset the carousel to the start position
*/
public void reset() {
scrollTo(0, 0);
setCurrentTab(TAB_INDEX_FIRST);
moveToYCoordinate(TAB_INDEX_FIRST, 0);
}
/**
* Store this information as the last requested Y coordinate for the given
* tabIndex.
*
* @param tabIndex The tab index being stored
* @param y The Y cooridinate to move to
*/
public void storeYCoordinate(int tabIndex, float y) {
Y_COORDINATE[tabIndex] = y;
}
/**
* Restore the Y position of this view to the last manually requested value.
* This can be done after the parent has been re-laid out again, where this
* view's position could have been lost if the view laid outside its
* parent's bounds.
*
* @param duration The duration of the animation
* @param tabIndex The index to restore
*/
public void restoreYCoordinate(int duration, int tabIndex) {
final float storedYCoordinate = getStoredYCoordinateForTab(tabIndex);
final Interpolator interpolator = AnimationUtils.loadInterpolator(getContext(),
android.R.anim.accelerate_decelerate_interpolator);
final ObjectAnimator animator = ObjectAnimator.ofFloat(this, "y", storedYCoordinate);
animator.addListener(mTabCarouselAnimatorListener);
animator.setInterpolator(interpolator);
animator.setDuration(duration);
animator.start();
}
/**
* Request that the view move to the given Y coordinate. Also store the Y
* coordinate as the last requested Y coordinate for the given tabIndex.
*
* @param tabIndex The tab index being stored
* @param y The Y cooridinate to move to
*/
public void moveToYCoordinate(int tabIndex, float y) {
storeYCoordinate(tabIndex, y);
restoreYCoordinate(0, tabIndex);
}
/**
* Used to propely call {@code #onMeasure(int, int)}
*
* @param yesOrNo Yes to indicate both tabs will be used in the carousel,
* false to indicate only one
*/
public void setUsesDualTabs(boolean yesOrNo) {
mDualTabs = yesOrNo;
}
/**
* Set the given {@link OnCarouselListener} to handle carousel events
*/
public void setListener(OnCarouselListener carouselListener) {
mCarouselListener = carouselListener;
}
/**
* Updates the tab selection
*
* @param position The index to update
*/
public void setCurrentTab(int position) {
final CarouselTab selected, deselected;
switch (position) {
case TAB_INDEX_FIRST:
selected = mFirstTab;
deselected = mSecondTab;
break;
case TAB_INDEX_SECOND:
selected = mSecondTab;
deselected = mFirstTab;
break;
default:
throw new IllegalStateException("Invalid tab position " + position);
}
selected.setSelected(true);
deselected.setSelected(false);
mCurrentTab = position;
}
/**
* Sets the label for a tab
*
* @param index Which label to write on
* @param label The string to set as the label
*/
public void setLabel(int index, String label) {
switch (index) {
case TAB_INDEX_FIRST:
mFirstTab.setLabel(label);
break;
case TAB_INDEX_SECOND:
mSecondTab.setLabel(label);
break;
default:
throw new IllegalStateException("Invalid tab position " + index);
}
}
/**
* Sets a drawable as the content of the tab {@link ImageView}
*
* @param index Which {@link ImageView}
* @param resId The resource identifier of the the drawable
*/
public void setImageResource(int index, int resId) {
switch (index) {
case TAB_INDEX_FIRST:
mFirstTab.setImageResource(resId);
break;
case TAB_INDEX_SECOND:
mSecondTab.setImageResource(resId);
break;
default:
throw new IllegalStateException("Invalid tab position " + index);
}
}
/**
* Sets a drawable as the content of the tab {@link ImageView}
*
* @param index Which {@link ImageView}
* @param drawable The {@link Drawable} to set
*/
public void setImageDrawable(int index, Drawable drawable) {
switch (index) {
case TAB_INDEX_FIRST:
mFirstTab.setImageDrawable(drawable);
break;
case TAB_INDEX_SECOND:
mSecondTab.setImageDrawable(drawable);
break;
default:
throw new IllegalStateException("Invalid tab position " + index);
}
}
/**
* Sets a bitmap as the content of the tab {@link ImageView}
*
* @param index Which {@link ImageView}
* @param bm The {@link Bitmap} to set
*/
public void setImageBitmap(int index, Bitmap bm) {
switch (index) {
case TAB_INDEX_FIRST:
mFirstTab.setImageBitmap(bm);
break;
case TAB_INDEX_SECOND:
mSecondTab.setImageBitmap(bm);
break;
default:
throw new IllegalStateException("Invalid tab position " + index);
}
}
/**
* Used to return the {@link ImageView} from one of the tabs
*
* @param index The index returning the {@link ImageView}
* @return The {@link ImageView} from one of the tabs
*/
public ImageView getImage(int index) {
switch (index) {
case TAB_INDEX_FIRST:
return mFirstTab.getImage();
case TAB_INDEX_SECOND:
return mSecondTab.getImage();
default:
throw new IllegalStateException("Invalid tab position " + index);
}
}
/**
* Used to return the label from one of the tabs
*
* @param index The index returning the label
* @return The label from one of the tabs
*/
public TextView getLabel(int index) {
switch (index) {
case TAB_INDEX_FIRST:
return mFirstTab.getLabel();
case TAB_INDEX_SECOND:
return mSecondTab.getLabel();
default:
throw new IllegalStateException("Invalid tab position " + index);
}
}
/**
* Returns the stored Y coordinate of this view the last time the user was
* on the selected tab given by tabIndex.
*
* @param tabIndex The tab index use to return the Y value
*/
public float getStoredYCoordinateForTab(int tabIndex) {
return Y_COORDINATE[tabIndex];
}
/**
* Returns the number of pixels that this view can be scrolled horizontally
*/
public int getAllowedHorizontalScrollLength() {
return mAllowedHorizontalScrollLength;
}
/**
* Returns the number of pixels that this view can be scrolled vertically
* while still allowing the tab labels to still show
*/
public int getAllowedVerticalScrollLength() {
return mAllowedVerticalScrollLength;
}
/**
* @param size The size of the measure specification
* @return The measure specifiction based on {@link MeasureSpec.#EXACTLY}
*/
private int measureExact(int size) {
return MeasureSpec.makeMeasureSpec(size, MeasureSpec.EXACTLY);
}
/**
* Sets the correct alpha layers over the tabs.
*/
private void updateAlphaLayers() {
float alpha = mLastScrollPosition * MAX_ALPHA / mAllowedHorizontalScrollLength;
alpha = Utils.clamp(alpha, 0.0f, 1.0f);
mFirstTab.setAlphaLayerValue(alpha);
mSecondTab.setAlphaLayerValue(MAX_ALPHA - alpha);
}
/**
* This listener keeps track of whether the tab carousel animation is
* currently going on or not, in order to prevent other simultaneous changes
* to the Y position of the tab carousel which can cause flicker.
*/
private final AnimatorListener mTabCarouselAnimatorListener = new AnimatorListener() {
/**
* {@inheritDoc}
*/
@Override
public void onAnimationCancel(Animator animation) {
mTabCarouselIsAnimating = false;
}
/**
* {@inheritDoc}
*/
@Override
public void onAnimationEnd(Animator animation) {
mTabCarouselIsAnimating = false;
}
/**
* {@inheritDoc}
*/
@Override
public void onAnimationRepeat(Animator animation) {
mTabCarouselIsAnimating = true;
}
/**
* {@inheritDoc}
*/
@Override
public void onAnimationStart(Animator animation) {
mTabCarouselIsAnimating = true;
}
};
/** When pressed, selects the corresponding tab */
private static final class TabClickListener implements OnClickListener {
/**
* Reference to {@link CarouselContainer}
*/
private final WeakReference<CarouselContainer> mReference;
/**
* The {@link CarouselTab} being pressed
*/
private final int mTab;
/**
* @param tab The index of the tab pressed
*/
public TabClickListener(CarouselContainer carouselHeader, int tab) {
super();
mReference = new WeakReference<CarouselContainer>(carouselHeader);
mTab = tab;
}
/**
* {@inheritDoc}
*/
@Override
public void onClick(View v) {
mReference.get().mCarouselListener.onTabSelected(mTab);
}
}
}