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