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