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