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