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