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