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