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