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