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