SlidingTab.java revision d8a3a8957b9d71ab75584b0cc98324fd70cc527c
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    /**
552     * Reset the tabs to their original state and stop any existing animation.
553     * Animate them back into place if animate is true.
554     *
555     * @param animate
556     */
557    public void reset(boolean animate) {
558        mLeftSlider.reset(animate);
559        mRightSlider.reset(animate);
560    }
561
562    @Override
563    public void setVisibility(int visibility) {
564        // Clear animations so sliders don't continue to animate when we show the widget again.
565        if (visibility != getVisibility() && visibility == View.INVISIBLE) {
566           reset(false);
567        }
568        super.setVisibility(visibility);
569    }
570
571    @Override
572    public boolean onTouchEvent(MotionEvent event) {
573        if (mTracking) {
574            final int action = event.getAction();
575            final float x = event.getX();
576            final float y = event.getY();
577
578            switch (action) {
579                case MotionEvent.ACTION_MOVE:
580                    if (withinView(x, y, this) ) {
581                        moveHandle(x, y);
582                        float position = isHorizontal() ? x : y;
583                        float target = mThreshold * (isHorizontal() ? getWidth() : getHeight());
584                        boolean thresholdReached;
585                        if (isHorizontal()) {
586                            thresholdReached = mCurrentSlider == mLeftSlider ?
587                                    position > target : position < target;
588                        } else {
589                            thresholdReached = mCurrentSlider == mLeftSlider ?
590                                    position < target : position > target;
591                        }
592                        if (!mTriggered && thresholdReached) {
593                            mTriggered = true;
594                            mTracking = false;
595                            mCurrentSlider.setState(Slider.STATE_ACTIVE);
596                            boolean isLeft = mCurrentSlider == mLeftSlider;
597                            dispatchTriggerEvent(isLeft ?
598                                OnTriggerListener.LEFT_HANDLE : OnTriggerListener.RIGHT_HANDLE);
599
600                            startAnimating(isLeft ? mHoldLeftOnTransition : mHoldRightOnTransition);
601                            setGrabbedState(OnTriggerListener.NO_HANDLE);
602                        }
603                        break;
604                    }
605                    // Intentionally fall through - we're outside tracking rectangle
606
607                case MotionEvent.ACTION_UP:
608                case MotionEvent.ACTION_CANCEL:
609                    mTracking = false;
610                    mTriggered = false;
611                    mOtherSlider.show(true);
612                    mCurrentSlider.reset(false);
613                    mCurrentSlider.hideTarget();
614                    mCurrentSlider = null;
615                    mOtherSlider = null;
616                    setGrabbedState(OnTriggerListener.NO_HANDLE);
617                    break;
618            }
619        }
620
621        return mTracking || super.onTouchEvent(event);
622    }
623
624    void startAnimating(final boolean holdAfter) {
625        mAnimating = true;
626        final Animation trans1;
627        final Animation trans2;
628        final Slider slider = mCurrentSlider;
629        final Slider other = mOtherSlider;
630        final int dx;
631        final int dy;
632        if (isHorizontal()) {
633            int right = slider.tab.getRight();
634            int width = slider.tab.getWidth();
635            int left = slider.tab.getLeft();
636            int viewWidth = getWidth();
637            int holdOffset = holdAfter ? 0 : width; // how much of tab to show at the end of anim
638            dx =  slider == mRightSlider ? - (right + viewWidth - holdOffset)
639                    : (viewWidth - left) + viewWidth - holdOffset;
640            dy = 0;
641        } else {
642            int top = slider.tab.getTop();
643            int bottom = slider.tab.getBottom();
644            int height = slider.tab.getHeight();
645            int viewHeight = getHeight();
646            int holdOffset = holdAfter ? 0 : height; // how much of tab to show at end of anim
647            dx = 0;
648            dy =  slider == mRightSlider ? (top + viewHeight - holdOffset)
649                    : - ((viewHeight - bottom) + viewHeight - holdOffset);
650        }
651        trans1 = new TranslateAnimation(0, dx, 0, dy);
652        trans1.setDuration(ANIM_DURATION);
653        trans1.setInterpolator(new LinearInterpolator());
654        trans1.setFillAfter(true);
655        trans2 = new TranslateAnimation(0, dx, 0, dy);
656        trans2.setDuration(ANIM_DURATION);
657        trans2.setInterpolator(new LinearInterpolator());
658        trans2.setFillAfter(true);
659
660        trans1.setAnimationListener(new AnimationListener() {
661            public void onAnimationEnd(Animation animation) {
662                Animation anim;
663                if (holdAfter) {
664                    anim = new TranslateAnimation(dx, dx, dy, dy);
665                    anim.setDuration(1000); // plenty of time for transitions
666                    mAnimating = false;
667                } else {
668                    anim = new AlphaAnimation(0.5f, 1.0f);
669                    anim.setDuration(ANIM_DURATION);
670                    resetView();
671                }
672                anim.setAnimationListener(mAnimationDoneListener);
673
674                /* Animation can be the same for these since the animation just holds */
675                mLeftSlider.startAnimation(anim, anim);
676                mRightSlider.startAnimation(anim, anim);
677            }
678
679            public void onAnimationRepeat(Animation animation) {
680
681            }
682
683            public void onAnimationStart(Animation animation) {
684
685            }
686
687        });
688
689        slider.hideTarget();
690        slider.startAnimation(trans1, trans2);
691    }
692
693    private void onAnimationDone() {
694        resetView();
695        mAnimating = false;
696    }
697
698    private boolean withinView(final float x, final float y, final View view) {
699        return isHorizontal() && y > - TRACKING_MARGIN && y < TRACKING_MARGIN + view.getHeight()
700            || !isHorizontal() && x > -TRACKING_MARGIN && x < TRACKING_MARGIN + view.getWidth();
701    }
702
703    private boolean isHorizontal() {
704        return mOrientation == HORIZONTAL;
705    }
706
707    private void resetView() {
708        mLeftSlider.reset(false);
709        mRightSlider.reset(false);
710        // onLayout(true, getLeft(), getTop(), getLeft() + getWidth(), getTop() + getHeight());
711    }
712
713    @Override
714    protected void onLayout(boolean changed, int l, int t, int r, int b) {
715        if (!changed) return;
716
717        // Center the widgets in the view
718        mLeftSlider.layout(l, t, r, b, isHorizontal() ? Slider.ALIGN_LEFT : Slider.ALIGN_BOTTOM);
719        mRightSlider.layout(l, t, r, b, isHorizontal() ? Slider.ALIGN_RIGHT : Slider.ALIGN_TOP);
720    }
721
722    private void moveHandle(float x, float y) {
723        final View handle = mCurrentSlider.tab;
724        final View content = mCurrentSlider.text;
725        if (isHorizontal()) {
726            int deltaX = (int) x - handle.getLeft() - (handle.getWidth() / 2);
727            handle.offsetLeftAndRight(deltaX);
728            content.offsetLeftAndRight(deltaX);
729        } else {
730            int deltaY = (int) y - handle.getTop() - (handle.getHeight() / 2);
731            handle.offsetTopAndBottom(deltaY);
732            content.offsetTopAndBottom(deltaY);
733        }
734        invalidate(); // TODO: be more conservative about what we're invalidating
735    }
736
737    /**
738     * Sets the left handle icon to a given resource.
739     *
740     * The resource should refer to a Drawable object, or use 0 to remove
741     * the icon.
742     *
743     * @param iconId the resource ID of the icon drawable
744     * @param targetId the resource of the target drawable
745     * @param barId the resource of the bar drawable (stateful)
746     * @param tabId the resource of the
747     */
748    public void setLeftTabResources(int iconId, int targetId, int barId, int tabId) {
749        mLeftSlider.setIcon(iconId);
750        mLeftSlider.setTarget(targetId);
751        mLeftSlider.setBarBackgroundResource(barId);
752        mLeftSlider.setTabBackgroundResource(tabId);
753        mLeftSlider.updateDrawableStates();
754    }
755
756    /**
757     * Sets the left handle hint text to a given resource string.
758     *
759     * @param resId
760     */
761    public void setLeftHintText(int resId) {
762        if (isHorizontal()) {
763            mLeftSlider.setHintText(resId);
764        }
765    }
766
767    /**
768     * Sets the right handle icon to a given resource.
769     *
770     * The resource should refer to a Drawable object, or use 0 to remove
771     * the icon.
772     *
773     * @param iconId the resource ID of the icon drawable
774     * @param targetId the resource of the target drawable
775     * @param barId the resource of the bar drawable (stateful)
776     * @param tabId the resource of the
777     */
778    public void setRightTabResources(int iconId, int targetId, int barId, int tabId) {
779        mRightSlider.setIcon(iconId);
780        mRightSlider.setTarget(targetId);
781        mRightSlider.setBarBackgroundResource(barId);
782        mRightSlider.setTabBackgroundResource(tabId);
783        mRightSlider.updateDrawableStates();
784    }
785
786    /**
787     * Sets the left handle hint text to a given resource string.
788     *
789     * @param resId
790     */
791    public void setRightHintText(int resId) {
792        if (isHorizontal()) {
793            mRightSlider.setHintText(resId);
794        }
795    }
796
797    public void setHoldAfterTrigger(boolean holdLeft, boolean holdRight) {
798        mHoldLeftOnTransition = holdLeft;
799        mHoldRightOnTransition = holdRight;
800    }
801
802    /**
803     * Triggers haptic feedback.
804     */
805    private synchronized void vibrate(long duration) {
806        if (mVibrator == null) {
807            mVibrator = (android.os.Vibrator)
808                    getContext().getSystemService(Context.VIBRATOR_SERVICE);
809        }
810        mVibrator.vibrate(duration);
811    }
812
813    /**
814     * Registers a callback to be invoked when the user triggers an event.
815     *
816     * @param listener the OnDialTriggerListener to attach to this view
817     */
818    public void setOnTriggerListener(OnTriggerListener listener) {
819        mOnTriggerListener = listener;
820    }
821
822    /**
823     * Dispatches a trigger event to listener. Ignored if a listener is not set.
824     * @param whichHandle the handle that triggered the event.
825     */
826    private void dispatchTriggerEvent(int whichHandle) {
827        vibrate(VIBRATE_LONG);
828        if (mOnTriggerListener != null) {
829            mOnTriggerListener.onTrigger(this, whichHandle);
830        }
831    }
832
833    /**
834     * Sets the current grabbed state, and dispatches a grabbed state change
835     * event to our listener.
836     */
837    private void setGrabbedState(int newState) {
838        if (newState != mGrabbedState) {
839            mGrabbedState = newState;
840            if (mOnTriggerListener != null) {
841                mOnTriggerListener.onGrabbedStateChange(this, mGrabbedState);
842            }
843        }
844    }
845
846    private void log(String msg) {
847        Log.d(LOG_TAG, msg);
848    }
849}
850