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