• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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          */
onTrigger(View v, int whichHandle)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          */
onGrabbedStateChange(View v, int grabbedState)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          */
Slider(ViewGroup parent, int tabId, int barId, int targetId)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 
setIcon(int iconId)225         void setIcon(int iconId) {
226             tab.setImageResource(iconId);
227         }
228 
setTabBackgroundResource(int tabId)229         void setTabBackgroundResource(int tabId) {
230             tab.setBackgroundResource(tabId);
231         }
232 
setBarBackgroundResource(int barId)233         void setBarBackgroundResource(int barId) {
234             text.setBackgroundResource(barId);
235         }
236 
setHintText(int resId)237         void setHintText(int resId) {
238             text.setText(resId);
239         }
240 
hide()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 
show(boolean animate)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 
setState(int state)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 
showTarget()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 
reset(boolean animate)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 
setTarget(int targetId)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          */
layout(int l, int t, int r, int b, int alignment)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 
updateDrawableStates()396         public void updateDrawableStates() {
397             setState(currentState);
398         }
399 
400         /**
401          * Ensure all the dependent widgets are measured.
402          */
measure(int widthMeasureSpec, int heightMeasureSpec)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          */
getTabWidth()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          */
getTabHeight()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          */
startAnimation(Animation anim1, Animation anim2)435         public void startAnimation(Animation anim1, Animation anim2) {
436             tab.startAnimation(anim1);
437             text.startAnimation(anim2);
438         }
439 
hideTarget()440         public void hideTarget() {
441             target.clearAnimation();
442             target.setVisibility(View.INVISIBLE);
443         }
444     }
445 
SlidingTab(Context context)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      */
SlidingTab(Context context, AttributeSet attrs)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
onMeasure(int widthMeasureSpec, int heightMeasureSpec)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
onInterceptTouchEvent(MotionEvent event)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      */
reset(boolean animate)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
setVisibility(int visibility)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
onTouchEvent(MotionEvent event)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