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