Home | History | Annotate | Download | only in widget
      1 /*
      2  * Copyright (C) 2009 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.internal.widget;
     18 
     19 import android.content.Context;
     20 import android.content.res.Configuration;
     21 import android.content.res.Resources;
     22 import android.content.res.TypedArray;
     23 import android.graphics.Canvas;
     24 import android.graphics.Rect;
     25 import android.graphics.drawable.Drawable;
     26 import android.os.Vibrator;
     27 import android.util.AttributeSet;
     28 import android.util.Log;
     29 import android.view.Gravity;
     30 import android.view.MotionEvent;
     31 import android.view.View;
     32 import android.view.ViewGroup;
     33 import android.view.animation.AlphaAnimation;
     34 import android.view.animation.Animation;
     35 import android.view.animation.LinearInterpolator;
     36 import android.view.animation.TranslateAnimation;
     37 import android.view.animation.Animation.AnimationListener;
     38 import android.widget.ImageView;
     39 import android.widget.TextView;
     40 import android.widget.ImageView.ScaleType;
     41 import com.android.internal.R;
     42 
     43 /**
     44  * A special widget containing two Sliders and a threshold for each.  Moving either slider beyond
     45  * the threshold will cause the registered OnTriggerListener.onTrigger() to be called with
     46  * whichHandle being {@link OnTriggerListener#LEFT_HANDLE} or {@link OnTriggerListener#RIGHT_HANDLE}
     47  * Equivalently, selecting a tab will result in a call to
     48  * {@link OnTriggerListener#onGrabbedStateChange(View, int)} with one of these two states. Releasing
     49  * the tab will result in whichHandle being {@link OnTriggerListener#NO_HANDLE}.
     50  *
     51  */
     52 public class SlidingTab extends ViewGroup {
     53     private static final String LOG_TAG = "SlidingTab";
     54     private static final boolean DBG = false;
     55     private static final int HORIZONTAL = 0; // as defined in attrs.xml
     56     private static final int VERTICAL = 1;
     57 
     58     // TODO: Make these configurable
     59     private static final float THRESHOLD = 2.0f / 3.0f;
     60     private static final long VIBRATE_SHORT = 30;
     61     private static final long VIBRATE_LONG = 40;
     62     private static final int TRACKING_MARGIN = 50;
     63     private static final int ANIM_DURATION = 250; // Time for most animations (in ms)
     64     private static final int ANIM_TARGET_TIME = 500; // Time to show targets (in ms)
     65     private boolean mHoldLeftOnTransition = true;
     66     private boolean mHoldRightOnTransition = true;
     67 
     68     private OnTriggerListener mOnTriggerListener;
     69     private int mGrabbedState = OnTriggerListener.NO_HANDLE;
     70     private boolean mTriggered = false;
     71     private Vibrator mVibrator;
     72     private float mDensity; // used to scale dimensions for bitmaps.
     73 
     74     /**
     75      * Either {@link #HORIZONTAL} or {@link #VERTICAL}.
     76      */
     77     private int mOrientation;
     78 
     79     private Slider mLeftSlider;
     80     private Slider mRightSlider;
     81     private Slider mCurrentSlider;
     82     private boolean mTracking;
     83     private float mThreshold;
     84     private Slider mOtherSlider;
     85     private boolean mAnimating;
     86     private Rect mTmpRect;
     87 
     88     /**
     89      * Listener used to reset the view when the current animation completes.
     90      */
     91     private final AnimationListener mAnimationDoneListener = new AnimationListener() {
     92         public void onAnimationStart(Animation animation) {
     93 
     94         }
     95 
     96         public void onAnimationRepeat(Animation animation) {
     97 
     98         }
     99 
    100         public void onAnimationEnd(Animation animation) {
    101             onAnimationDone();
    102         }
    103     };
    104 
    105     /**
    106      * Interface definition for a callback to be invoked when a tab is triggered
    107      * by moving it beyond a threshold.
    108      */
    109     public interface OnTriggerListener {
    110         /**
    111          * The interface was triggered because the user let go of the handle without reaching the
    112          * threshold.
    113          */
    114         public static final int NO_HANDLE = 0;
    115 
    116         /**
    117          * The interface was triggered because the user grabbed the left handle and moved it past
    118          * the threshold.
    119          */
    120         public static final int LEFT_HANDLE = 1;
    121 
    122         /**
    123          * The interface was triggered because the user grabbed the right handle and moved it past
    124          * the threshold.
    125          */
    126         public static final int RIGHT_HANDLE = 2;
    127 
    128         /**
    129          * Called when the user moves a handle beyond the threshold.
    130          *
    131          * @param v The view that was triggered.
    132          * @param whichHandle  Which "dial handle" the user grabbed,
    133          *        either {@link #LEFT_HANDLE}, {@link #RIGHT_HANDLE}.
    134          */
    135         void onTrigger(View v, int whichHandle);
    136 
    137         /**
    138          * Called when the "grabbed state" changes (i.e. when the user either grabs or releases
    139          * one of the handles.)
    140          *
    141          * @param v the view that was triggered
    142          * @param grabbedState the new state: {@link #NO_HANDLE}, {@link #LEFT_HANDLE},
    143          * or {@link #RIGHT_HANDLE}.
    144          */
    145         void onGrabbedStateChange(View v, int grabbedState);
    146     }
    147 
    148     /**
    149      * Simple container class for all things pertinent to a slider.
    150      * A slider consists of 3 Views:
    151      *
    152      * {@link #tab} is the tab shown on the screen in the default state.
    153      * {@link #text} is the view revealed as the user slides the tab out.
    154      * {@link #target} is the target the user must drag the slider past to trigger the slider.
    155      *
    156      */
    157     private static class Slider {
    158         /**
    159          * Tab alignment - determines which side the tab should be drawn on
    160          */
    161         public static final int ALIGN_LEFT = 0;
    162         public static final int ALIGN_RIGHT = 1;
    163         public static final int ALIGN_TOP = 2;
    164         public static final int ALIGN_BOTTOM = 3;
    165         public static final int ALIGN_UNKNOWN = 4;
    166 
    167         /**
    168          * States for the view.
    169          */
    170         private static final int STATE_NORMAL = 0;
    171         private static final int STATE_PRESSED = 1;
    172         private static final int STATE_ACTIVE = 2;
    173 
    174         private final ImageView tab;
    175         private final TextView text;
    176         private final ImageView target;
    177         private int currentState = STATE_NORMAL;
    178         private int alignment = ALIGN_UNKNOWN;
    179         private int alignment_value;
    180 
    181         /**
    182          * Constructor
    183          *
    184          * @param parent the container view of this one
    185          * @param tabId drawable for the tab
    186          * @param barId drawable for the bar
    187          * @param targetId drawable for the target
    188          */
    189         Slider(ViewGroup parent, int tabId, int barId, int targetId) {
    190             // Create tab
    191             tab = new ImageView(parent.getContext());
    192             tab.setBackgroundResource(tabId);
    193             tab.setScaleType(ScaleType.CENTER);
    194             tab.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT,
    195                     LayoutParams.WRAP_CONTENT));
    196 
    197             // Create hint TextView
    198             text = new TextView(parent.getContext());
    199             text.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT,
    200                     LayoutParams.MATCH_PARENT));
    201             text.setBackgroundResource(barId);
    202             text.setTextAppearance(parent.getContext(), R.style.TextAppearance_SlidingTabNormal);
    203             // hint.setSingleLine();  // Hmm.. this causes the text to disappear off-screen
    204 
    205             // Create target
    206             target = new ImageView(parent.getContext());
    207             target.setImageResource(targetId);
    208             target.setScaleType(ScaleType.CENTER);
    209             target.setLayoutParams(
    210                     new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));
    211             target.setVisibility(View.INVISIBLE);
    212 
    213             parent.addView(target); // this needs to be first - relies on painter's algorithm
    214             parent.addView(tab);
    215             parent.addView(text);
    216         }
    217 
    218         void setIcon(int iconId) {
    219             tab.setImageResource(iconId);
    220         }
    221 
    222         void setTabBackgroundResource(int tabId) {
    223             tab.setBackgroundResource(tabId);
    224         }
    225 
    226         void setBarBackgroundResource(int barId) {
    227             text.setBackgroundResource(barId);
    228         }
    229 
    230         void setHintText(int resId) {
    231             text.setText(resId);
    232         }
    233 
    234         void hide() {
    235             boolean horiz = alignment == ALIGN_LEFT || alignment == ALIGN_RIGHT;
    236             int dx = horiz ? (alignment == ALIGN_LEFT ? alignment_value - tab.getRight()
    237                     : alignment_value - tab.getLeft()) : 0;
    238             int dy = horiz ? 0 : (alignment == ALIGN_TOP ? alignment_value - tab.getBottom()
    239                     : alignment_value - tab.getTop());
    240 
    241             Animation trans = new TranslateAnimation(0, dx, 0, dy);
    242             trans.setDuration(ANIM_DURATION);
    243             trans.setFillAfter(true);
    244             tab.startAnimation(trans);
    245             text.startAnimation(trans);
    246             target.setVisibility(View.INVISIBLE);
    247         }
    248 
    249         void show(boolean animate) {
    250             text.setVisibility(View.VISIBLE);
    251             tab.setVisibility(View.VISIBLE);
    252             //target.setVisibility(View.INVISIBLE);
    253             if (animate) {
    254                 boolean horiz = alignment == ALIGN_LEFT || alignment == ALIGN_RIGHT;
    255                 int dx = horiz ? (alignment == ALIGN_LEFT ? tab.getWidth() : -tab.getWidth()) : 0;
    256                 int dy = horiz ? 0: (alignment == ALIGN_TOP ? tab.getHeight() : -tab.getHeight());
    257 
    258                 Animation trans = new TranslateAnimation(-dx, 0, -dy, 0);
    259                 trans.setDuration(ANIM_DURATION);
    260                 tab.startAnimation(trans);
    261                 text.startAnimation(trans);
    262             }
    263         }
    264 
    265         void setState(int state) {
    266             text.setPressed(state == STATE_PRESSED);
    267             tab.setPressed(state == STATE_PRESSED);
    268             if (state == STATE_ACTIVE) {
    269                 final int[] activeState = new int[] {com.android.internal.R.attr.state_active};
    270                 if (text.getBackground().isStateful()) {
    271                     text.getBackground().setState(activeState);
    272                 }
    273                 if (tab.getBackground().isStateful()) {
    274                     tab.getBackground().setState(activeState);
    275                 }
    276                 text.setTextAppearance(text.getContext(), R.style.TextAppearance_SlidingTabActive);
    277             } else {
    278                 text.setTextAppearance(text.getContext(), R.style.TextAppearance_SlidingTabNormal);
    279             }
    280             currentState = state;
    281         }
    282 
    283         void showTarget() {
    284             AlphaAnimation alphaAnim = new AlphaAnimation(0.0f, 1.0f);
    285             alphaAnim.setDuration(ANIM_TARGET_TIME);
    286             target.startAnimation(alphaAnim);
    287             target.setVisibility(View.VISIBLE);
    288         }
    289 
    290         void reset(boolean animate) {
    291             setState(STATE_NORMAL);
    292             text.setVisibility(View.VISIBLE);
    293             text.setTextAppearance(text.getContext(), R.style.TextAppearance_SlidingTabNormal);
    294             tab.setVisibility(View.VISIBLE);
    295             target.setVisibility(View.INVISIBLE);
    296             final boolean horiz = alignment == ALIGN_LEFT || alignment == ALIGN_RIGHT;
    297             int dx = horiz ? (alignment == ALIGN_LEFT ?  alignment_value - tab.getLeft()
    298                     : alignment_value - tab.getRight()) : 0;
    299             int dy = horiz ? 0 : (alignment == ALIGN_TOP ? alignment_value - tab.getTop()
    300                     : alignment_value - tab.getBottom());
    301             if (animate) {
    302                 TranslateAnimation trans = new TranslateAnimation(0, dx, 0, dy);
    303                 trans.setDuration(ANIM_DURATION);
    304                 trans.setFillAfter(false);
    305                 text.startAnimation(trans);
    306                 tab.startAnimation(trans);
    307             } else {
    308                 if (horiz) {
    309                     text.offsetLeftAndRight(dx);
    310                     tab.offsetLeftAndRight(dx);
    311                 } else {
    312                     text.offsetTopAndBottom(dy);
    313                     tab.offsetTopAndBottom(dy);
    314                 }
    315                 text.clearAnimation();
    316                 tab.clearAnimation();
    317                 target.clearAnimation();
    318             }
    319         }
    320 
    321         void setTarget(int targetId) {
    322             target.setImageResource(targetId);
    323         }
    324 
    325         /**
    326          * Layout the given widgets within the parent.
    327          *
    328          * @param l the parent's left border
    329          * @param t the parent's top border
    330          * @param r the parent's right border
    331          * @param b the parent's bottom border
    332          * @param alignment which side to align the widget to
    333          */
    334         void layout(int l, int t, int r, int b, int alignment) {
    335             this.alignment = alignment;
    336             final Drawable tabBackground = tab.getBackground();
    337             final int handleWidth = tabBackground.getIntrinsicWidth();
    338             final int handleHeight = tabBackground.getIntrinsicHeight();
    339             final Drawable targetDrawable = target.getDrawable();
    340             final int targetWidth = targetDrawable.getIntrinsicWidth();
    341             final int targetHeight = targetDrawable.getIntrinsicHeight();
    342             final int parentWidth = r - l;
    343             final int parentHeight = b - t;
    344 
    345             final int leftTarget = (int) (THRESHOLD * parentWidth) - targetWidth + handleWidth / 2;
    346             final int rightTarget = (int) ((1.0f - THRESHOLD) * parentWidth) - handleWidth / 2;
    347             final int left = (parentWidth - handleWidth) / 2;
    348             final int right = left + handleWidth;
    349 
    350             if (alignment == ALIGN_LEFT || alignment == ALIGN_RIGHT) {
    351                 // horizontal
    352                 final int targetTop = (parentHeight - targetHeight) / 2;
    353                 final int targetBottom = targetTop + targetHeight;
    354                 final int top = (parentHeight - handleHeight) / 2;
    355                 final int bottom = (parentHeight + handleHeight) / 2;
    356                 if (alignment == ALIGN_LEFT) {
    357                     tab.layout(0, top, handleWidth, bottom);
    358                     text.layout(0 - parentWidth, top, 0, bottom);
    359                     text.setGravity(Gravity.RIGHT);
    360                     target.layout(leftTarget, targetTop, leftTarget + targetWidth, targetBottom);
    361                     alignment_value = l;
    362                 } else {
    363                     tab.layout(parentWidth - handleWidth, top, parentWidth, bottom);
    364                     text.layout(parentWidth, top, parentWidth + parentWidth, bottom);
    365                     target.layout(rightTarget, targetTop, rightTarget + targetWidth, targetBottom);
    366                     text.setGravity(Gravity.TOP);
    367                     alignment_value = r;
    368                 }
    369             } else {
    370                 // vertical
    371                 final int targetLeft = (parentWidth - targetWidth) / 2;
    372                 final int targetRight = (parentWidth + targetWidth) / 2;
    373                 final int top = (int) (THRESHOLD * parentHeight) + handleHeight / 2 - targetHeight;
    374                 final int bottom = (int) ((1.0f - THRESHOLD) * parentHeight) - handleHeight / 2;
    375                 if (alignment == ALIGN_TOP) {
    376                     tab.layout(left, 0, right, handleHeight);
    377                     text.layout(left, 0 - parentHeight, right, 0);
    378                     target.layout(targetLeft, top, targetRight, top + targetHeight);
    379                     alignment_value = t;
    380                 } else {
    381                     tab.layout(left, parentHeight - handleHeight, right, parentHeight);
    382                     text.layout(left, parentHeight, right, parentHeight + parentHeight);
    383                     target.layout(targetLeft, bottom, targetRight, bottom + targetHeight);
    384                     alignment_value = b;
    385                 }
    386             }
    387         }
    388 
    389         public void updateDrawableStates() {
    390             setState(currentState);
    391         }
    392 
    393         /**
    394          * Ensure all the dependent widgets are measured.
    395          */
    396         public void measure() {
    397             tab.measure(View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
    398                     View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED));
    399             text.measure(View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
    400                     View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED));
    401         }
    402 
    403         /**
    404          * Get the measured tab width. Must be called after {@link Slider#measure()}.
    405          * @return
    406          */
    407         public int getTabWidth() {
    408             return tab.getMeasuredWidth();
    409         }
    410 
    411         /**
    412          * Get the measured tab width. Must be called after {@link Slider#measure()}.
    413          * @return
    414          */
    415         public int getTabHeight() {
    416             return tab.getMeasuredHeight();
    417         }
    418 
    419         /**
    420          * Start animating the slider. Note we need two animations since an Animator
    421          * keeps internal state of the invalidation region which is just the view being animated.
    422          *
    423          * @param anim1
    424          * @param anim2
    425          */
    426         public void startAnimation(Animation anim1, Animation anim2) {
    427             tab.startAnimation(anim1);
    428             text.startAnimation(anim2);
    429         }
    430 
    431         public void hideTarget() {
    432             target.clearAnimation();
    433             target.setVisibility(View.INVISIBLE);
    434         }
    435     }
    436 
    437     public SlidingTab(Context context) {
    438         this(context, null);
    439     }
    440 
    441     /**
    442      * Constructor used when this widget is created from a layout file.
    443      */
    444     public SlidingTab(Context context, AttributeSet attrs) {
    445         super(context, attrs);
    446 
    447         // Allocate a temporary once that can be used everywhere.
    448         mTmpRect = new Rect();
    449 
    450         TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SlidingTab);
    451         mOrientation = a.getInt(R.styleable.SlidingTab_orientation, HORIZONTAL);
    452         a.recycle();
    453 
    454         Resources r = getResources();
    455         mDensity = r.getDisplayMetrics().density;
    456         if (DBG) log("- Density: " + mDensity);
    457 
    458         mLeftSlider = new Slider(this,
    459                 R.drawable.jog_tab_left_generic,
    460                 R.drawable.jog_tab_bar_left_generic,
    461                 R.drawable.jog_tab_target_gray);
    462         mRightSlider = new Slider(this,
    463                 R.drawable.jog_tab_right_generic,
    464                 R.drawable.jog_tab_bar_right_generic,
    465                 R.drawable.jog_tab_target_gray);
    466 
    467         // setBackgroundColor(0x80808080);
    468     }
    469 
    470     @Override
    471     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    472         int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
    473         int widthSpecSize =  MeasureSpec.getSize(widthMeasureSpec);
    474 
    475         int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
    476         int heightSpecSize =  MeasureSpec.getSize(heightMeasureSpec);
    477 
    478         if (widthSpecMode == MeasureSpec.UNSPECIFIED || heightSpecMode == MeasureSpec.UNSPECIFIED) {
    479             Log.e("SlidingTab", "SlidingTab cannot have UNSPECIFIED MeasureSpec"
    480                     +"(wspec=" + widthSpecMode + ", hspec=" + heightSpecMode + ")",
    481                     new RuntimeException(LOG_TAG + "stack:"));
    482         }
    483 
    484         mLeftSlider.measure();
    485         mRightSlider.measure();
    486         final int leftTabWidth = mLeftSlider.getTabWidth();
    487         final int rightTabWidth = mRightSlider.getTabWidth();
    488         final int leftTabHeight = mLeftSlider.getTabHeight();
    489         final int rightTabHeight = mRightSlider.getTabHeight();
    490         final int width;
    491         final int height;
    492         if (isHorizontal()) {
    493             width = Math.max(widthSpecSize, leftTabWidth + rightTabWidth);
    494             height = Math.max(leftTabHeight, rightTabHeight);
    495         } else {
    496             width = Math.max(leftTabWidth, rightTabHeight);
    497             height = Math.max(heightSpecSize, leftTabHeight + rightTabHeight);
    498         }
    499         setMeasuredDimension(width, height);
    500     }
    501 
    502     @Override
    503     public boolean onInterceptTouchEvent(MotionEvent event) {
    504         final int action = event.getAction();
    505         final float x = event.getX();
    506         final float y = event.getY();
    507 
    508         if (mAnimating) {
    509             return false;
    510         }
    511 
    512         View leftHandle = mLeftSlider.tab;
    513         leftHandle.getHitRect(mTmpRect);
    514         boolean leftHit = mTmpRect.contains((int) x, (int) y);
    515 
    516         View rightHandle = mRightSlider.tab;
    517         rightHandle.getHitRect(mTmpRect);
    518         boolean rightHit = mTmpRect.contains((int)x, (int) y);
    519 
    520         if (!mTracking && !(leftHit || rightHit)) {
    521             return false;
    522         }
    523 
    524         switch (action) {
    525             case MotionEvent.ACTION_DOWN: {
    526                 mTracking = true;
    527                 mTriggered = false;
    528                 vibrate(VIBRATE_SHORT);
    529                 if (leftHit) {
    530                     mCurrentSlider = mLeftSlider;
    531                     mOtherSlider = mRightSlider;
    532                     mThreshold = isHorizontal() ? THRESHOLD : 1.0f - THRESHOLD;
    533                     setGrabbedState(OnTriggerListener.LEFT_HANDLE);
    534                 } else {
    535                     mCurrentSlider = mRightSlider;
    536                     mOtherSlider = mLeftSlider;
    537                     mThreshold = isHorizontal() ? 1.0f - THRESHOLD : THRESHOLD;
    538                     setGrabbedState(OnTriggerListener.RIGHT_HANDLE);
    539                 }
    540                 mCurrentSlider.setState(Slider.STATE_PRESSED);
    541                 mCurrentSlider.showTarget();
    542                 mOtherSlider.hide();
    543                 break;
    544             }
    545         }
    546 
    547         return true;
    548     }
    549 
    550     /**
    551      * Reset the tabs to their original state and stop any existing animation.
    552      * Animate them back into place if animate is true.
    553      *
    554      * @param animate
    555      */
    556     public void reset(boolean animate) {
    557         mLeftSlider.reset(animate);
    558         mRightSlider.reset(animate);
    559         if (!animate) {
    560             mAnimating = false;
    561         }
    562     }
    563 
    564     @Override
    565     public void setVisibility(int visibility) {
    566         // Clear animations so sliders don't continue to animate when we show the widget again.
    567         if (visibility != getVisibility() && visibility == View.INVISIBLE) {
    568            reset(false);
    569         }
    570         super.setVisibility(visibility);
    571     }
    572 
    573     @Override
    574     public boolean onTouchEvent(MotionEvent event) {
    575         if (mTracking) {
    576             final int action = event.getAction();
    577             final float x = event.getX();
    578             final float y = event.getY();
    579 
    580             switch (action) {
    581                 case MotionEvent.ACTION_MOVE:
    582                     if (withinView(x, y, this) ) {
    583                         moveHandle(x, y);
    584                         float position = isHorizontal() ? x : y;
    585                         float target = mThreshold * (isHorizontal() ? getWidth() : getHeight());
    586                         boolean thresholdReached;
    587                         if (isHorizontal()) {
    588                             thresholdReached = mCurrentSlider == mLeftSlider ?
    589                                     position > target : position < target;
    590                         } else {
    591                             thresholdReached = mCurrentSlider == mLeftSlider ?
    592                                     position < target : position > target;
    593                         }
    594                         if (!mTriggered && thresholdReached) {
    595                             mTriggered = true;
    596                             mTracking = false;
    597                             mCurrentSlider.setState(Slider.STATE_ACTIVE);
    598                             boolean isLeft = mCurrentSlider == mLeftSlider;
    599                             dispatchTriggerEvent(isLeft ?
    600                                 OnTriggerListener.LEFT_HANDLE : OnTriggerListener.RIGHT_HANDLE);
    601 
    602                             startAnimating(isLeft ? mHoldLeftOnTransition : mHoldRightOnTransition);
    603                             setGrabbedState(OnTriggerListener.NO_HANDLE);
    604                         }
    605                         break;
    606                     }
    607                     // Intentionally fall through - we're outside tracking rectangle
    608 
    609                 case MotionEvent.ACTION_UP:
    610                 case MotionEvent.ACTION_CANCEL:
    611                     mTracking = false;
    612                     mTriggered = false;
    613                     mOtherSlider.show(true);
    614                     mCurrentSlider.reset(false);
    615                     mCurrentSlider.hideTarget();
    616                     mCurrentSlider = null;
    617                     mOtherSlider = null;
    618                     setGrabbedState(OnTriggerListener.NO_HANDLE);
    619                     break;
    620             }
    621         }
    622 
    623         return mTracking || super.onTouchEvent(event);
    624     }
    625 
    626     void startAnimating(final boolean holdAfter) {
    627         mAnimating = true;
    628         final Animation trans1;
    629         final Animation trans2;
    630         final Slider slider = mCurrentSlider;
    631         final Slider other = mOtherSlider;
    632         final int dx;
    633         final int dy;
    634         if (isHorizontal()) {
    635             int right = slider.tab.getRight();
    636             int width = slider.tab.getWidth();
    637             int left = slider.tab.getLeft();
    638             int viewWidth = getWidth();
    639             int holdOffset = holdAfter ? 0 : width; // how much of tab to show at the end of anim
    640             dx =  slider == mRightSlider ? - (right + viewWidth - holdOffset)
    641                     : (viewWidth - left) + viewWidth - holdOffset;
    642             dy = 0;
    643         } else {
    644             int top = slider.tab.getTop();
    645             int bottom = slider.tab.getBottom();
    646             int height = slider.tab.getHeight();
    647             int viewHeight = getHeight();
    648             int holdOffset = holdAfter ? 0 : height; // how much of tab to show at end of anim
    649             dx = 0;
    650             dy =  slider == mRightSlider ? (top + viewHeight - holdOffset)
    651                     : - ((viewHeight - bottom) + viewHeight - holdOffset);
    652         }
    653         trans1 = new TranslateAnimation(0, dx, 0, dy);
    654         trans1.setDuration(ANIM_DURATION);
    655         trans1.setInterpolator(new LinearInterpolator());
    656         trans1.setFillAfter(true);
    657         trans2 = new TranslateAnimation(0, dx, 0, dy);
    658         trans2.setDuration(ANIM_DURATION);
    659         trans2.setInterpolator(new LinearInterpolator());
    660         trans2.setFillAfter(true);
    661 
    662         trans1.setAnimationListener(new AnimationListener() {
    663             public void onAnimationEnd(Animation animation) {
    664                 Animation anim;
    665                 if (holdAfter) {
    666                     anim = new TranslateAnimation(dx, dx, dy, dy);
    667                     anim.setDuration(1000); // plenty of time for transitions
    668                     mAnimating = false;
    669                 } else {
    670                     anim = new AlphaAnimation(0.5f, 1.0f);
    671                     anim.setDuration(ANIM_DURATION);
    672                     resetView();
    673                 }
    674                 anim.setAnimationListener(mAnimationDoneListener);
    675 
    676                 /* Animation can be the same for these since the animation just holds */
    677                 mLeftSlider.startAnimation(anim, anim);
    678                 mRightSlider.startAnimation(anim, anim);
    679             }
    680 
    681             public void onAnimationRepeat(Animation animation) {
    682 
    683             }
    684 
    685             public void onAnimationStart(Animation animation) {
    686 
    687             }
    688 
    689         });
    690 
    691         slider.hideTarget();
    692         slider.startAnimation(trans1, trans2);
    693     }
    694 
    695     private void onAnimationDone() {
    696         resetView();
    697         mAnimating = false;
    698     }
    699 
    700     private boolean withinView(final float x, final float y, final View view) {
    701         return isHorizontal() && y > - TRACKING_MARGIN && y < TRACKING_MARGIN + view.getHeight()
    702             || !isHorizontal() && x > -TRACKING_MARGIN && x < TRACKING_MARGIN + view.getWidth();
    703     }
    704 
    705     private boolean isHorizontal() {
    706         return mOrientation == HORIZONTAL;
    707     }
    708 
    709     private void resetView() {
    710         mLeftSlider.reset(false);
    711         mRightSlider.reset(false);
    712         // onLayout(true, getLeft(), getTop(), getLeft() + getWidth(), getTop() + getHeight());
    713     }
    714 
    715     @Override
    716     protected void onLayout(boolean changed, int l, int t, int r, int b) {
    717         if (!changed) return;
    718 
    719         // Center the widgets in the view
    720         mLeftSlider.layout(l, t, r, b, isHorizontal() ? Slider.ALIGN_LEFT : Slider.ALIGN_BOTTOM);
    721         mRightSlider.layout(l, t, r, b, isHorizontal() ? Slider.ALIGN_RIGHT : Slider.ALIGN_TOP);
    722     }
    723 
    724     private void moveHandle(float x, float y) {
    725         final View handle = mCurrentSlider.tab;
    726         final View content = mCurrentSlider.text;
    727         if (isHorizontal()) {
    728             int deltaX = (int) x - handle.getLeft() - (handle.getWidth() / 2);
    729             handle.offsetLeftAndRight(deltaX);
    730             content.offsetLeftAndRight(deltaX);
    731         } else {
    732             int deltaY = (int) y - handle.getTop() - (handle.getHeight() / 2);
    733             handle.offsetTopAndBottom(deltaY);
    734             content.offsetTopAndBottom(deltaY);
    735         }
    736         invalidate(); // TODO: be more conservative about what we're invalidating
    737     }
    738 
    739     /**
    740      * Sets the left handle icon to a given resource.
    741      *
    742      * The resource should refer to a Drawable object, or use 0 to remove
    743      * the icon.
    744      *
    745      * @param iconId the resource ID of the icon drawable
    746      * @param targetId the resource of the target drawable
    747      * @param barId the resource of the bar drawable (stateful)
    748      * @param tabId the resource of the
    749      */
    750     public void setLeftTabResources(int iconId, int targetId, int barId, int tabId) {
    751         mLeftSlider.setIcon(iconId);
    752         mLeftSlider.setTarget(targetId);
    753         mLeftSlider.setBarBackgroundResource(barId);
    754         mLeftSlider.setTabBackgroundResource(tabId);
    755         mLeftSlider.updateDrawableStates();
    756     }
    757 
    758     /**
    759      * Sets the left handle hint text to a given resource string.
    760      *
    761      * @param resId
    762      */
    763     public void setLeftHintText(int resId) {
    764         if (isHorizontal()) {
    765             mLeftSlider.setHintText(resId);
    766         }
    767     }
    768 
    769     /**
    770      * Sets the right handle icon to a given resource.
    771      *
    772      * The resource should refer to a Drawable object, or use 0 to remove
    773      * the icon.
    774      *
    775      * @param iconId the resource ID of the icon drawable
    776      * @param targetId the resource of the target drawable
    777      * @param barId the resource of the bar drawable (stateful)
    778      * @param tabId the resource of the
    779      */
    780     public void setRightTabResources(int iconId, int targetId, int barId, int tabId) {
    781         mRightSlider.setIcon(iconId);
    782         mRightSlider.setTarget(targetId);
    783         mRightSlider.setBarBackgroundResource(barId);
    784         mRightSlider.setTabBackgroundResource(tabId);
    785         mRightSlider.updateDrawableStates();
    786     }
    787 
    788     /**
    789      * Sets the left handle hint text to a given resource string.
    790      *
    791      * @param resId
    792      */
    793     public void setRightHintText(int resId) {
    794         if (isHorizontal()) {
    795             mRightSlider.setHintText(resId);
    796         }
    797     }
    798 
    799     public void setHoldAfterTrigger(boolean holdLeft, boolean holdRight) {
    800         mHoldLeftOnTransition = holdLeft;
    801         mHoldRightOnTransition = holdRight;
    802     }
    803 
    804     /**
    805      * Triggers haptic feedback.
    806      */
    807     private synchronized void vibrate(long duration) {
    808         if (mVibrator == null) {
    809             mVibrator = (android.os.Vibrator)
    810                     getContext().getSystemService(Context.VIBRATOR_SERVICE);
    811         }
    812         mVibrator.vibrate(duration);
    813     }
    814 
    815     /**
    816      * Registers a callback to be invoked when the user triggers an event.
    817      *
    818      * @param listener the OnDialTriggerListener to attach to this view
    819      */
    820     public void setOnTriggerListener(OnTriggerListener listener) {
    821         mOnTriggerListener = listener;
    822     }
    823 
    824     /**
    825      * Dispatches a trigger event to listener. Ignored if a listener is not set.
    826      * @param whichHandle the handle that triggered the event.
    827      */
    828     private void dispatchTriggerEvent(int whichHandle) {
    829         vibrate(VIBRATE_LONG);
    830         if (mOnTriggerListener != null) {
    831             mOnTriggerListener.onTrigger(this, whichHandle);
    832         }
    833     }
    834 
    835     /**
    836      * Sets the current grabbed state, and dispatches a grabbed state change
    837      * event to our listener.
    838      */
    839     private void setGrabbedState(int newState) {
    840         if (newState != mGrabbedState) {
    841             mGrabbedState = newState;
    842             if (mOnTriggerListener != null) {
    843                 mOnTriggerListener.onGrabbedStateChange(this, mGrabbedState);
    844             }
    845         }
    846     }
    847 
    848     private void log(String msg) {
    849         Log.d(LOG_TAG, msg);
    850     }
    851 }
    852