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