Home | History | Annotate | Download | only in widget
      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 
     17 package com.android.internal.widget;
     18 
     19 import android.content.Context;
     20 import android.content.res.Resources;
     21 import android.content.res.TypedArray;
     22 import android.graphics.Rect;
     23 import android.graphics.drawable.Drawable;
     24 import android.media.AudioAttributes;
     25 import android.os.UserHandle;
     26 import android.os.Vibrator;
     27 import android.provider.Settings;
     28 import android.util.AttributeSet;
     29 import android.util.Log;
     30 import android.view.Gravity;
     31 import android.view.MotionEvent;
     32 import android.view.View;
     33 import android.view.ViewGroup;
     34 import android.view.animation.AlphaAnimation;
     35 import android.view.animation.Animation;
     36 import android.view.animation.LinearInterpolator;
     37 import android.view.animation.TranslateAnimation;
     38 import android.view.animation.Animation.AnimationListener;
     39 import android.widget.ImageView;
     40 import android.widget.TextView;
     41 import android.widget.ImageView.ScaleType;
     42 
     43 import 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  */
     54 public 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() {
    404             tab.measure(View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
    405                     View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED));
    406             text.measure(View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
    407                     View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED));
    408         }
    409 
    410         /**
    411          * Get the measured tab width. Must be called after {@link Slider#measure()}.
    412          * @return
    413          */
    414         public int getTabWidth() {
    415             return tab.getMeasuredWidth();
    416         }
    417 
    418         /**
    419          * Get the measured tab width. Must be called after {@link Slider#measure()}.
    420          * @return
    421          */
    422         public int getTabHeight() {
    423             return tab.getMeasuredHeight();
    424         }
    425 
    426         /**
    427          * Start animating the slider. Note we need two animations since a ValueAnimator
    428          * keeps internal state of the invalidation region which is just the view being animated.
    429          *
    430          * @param anim1
    431          * @param anim2
    432          */
    433         public void startAnimation(Animation anim1, Animation anim2) {
    434             tab.startAnimation(anim1);
    435             text.startAnimation(anim2);
    436         }
    437 
    438         public void hideTarget() {
    439             target.clearAnimation();
    440             target.setVisibility(View.INVISIBLE);
    441         }
    442     }
    443 
    444     public SlidingTab(Context context) {
    445         this(context, null);
    446     }
    447 
    448     /**
    449      * Constructor used when this widget is created from a layout file.
    450      */
    451     public SlidingTab(Context context, AttributeSet attrs) {
    452         super(context, attrs);
    453 
    454         // Allocate a temporary once that can be used everywhere.
    455         mTmpRect = new Rect();
    456 
    457         TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SlidingTab);
    458         mOrientation = a.getInt(R.styleable.SlidingTab_orientation, HORIZONTAL);
    459         a.recycle();
    460 
    461         Resources r = getResources();
    462         mDensity = r.getDisplayMetrics().density;
    463         if (DBG) log("- Density: " + mDensity);
    464 
    465         mLeftSlider = new Slider(this,
    466                 R.drawable.jog_tab_left_generic,
    467                 R.drawable.jog_tab_bar_left_generic,
    468                 R.drawable.jog_tab_target_gray);
    469         mRightSlider = new Slider(this,
    470                 R.drawable.jog_tab_right_generic,
    471                 R.drawable.jog_tab_bar_right_generic,
    472                 R.drawable.jog_tab_target_gray);
    473 
    474         // setBackgroundColor(0x80808080);
    475     }
    476 
    477     @Override
    478     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    479         int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
    480         int widthSpecSize =  MeasureSpec.getSize(widthMeasureSpec);
    481 
    482         int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
    483         int heightSpecSize =  MeasureSpec.getSize(heightMeasureSpec);
    484 
    485         if (DBG) {
    486             if (widthSpecMode == MeasureSpec.UNSPECIFIED
    487                     || heightSpecMode == MeasureSpec.UNSPECIFIED) {
    488                 Log.e("SlidingTab", "SlidingTab cannot have UNSPECIFIED MeasureSpec"
    489                         +"(wspec=" + widthSpecMode + ", hspec=" + heightSpecMode + ")",
    490                         new RuntimeException(LOG_TAG + "stack:"));
    491             }
    492         }
    493 
    494         mLeftSlider.measure();
    495         mRightSlider.measure();
    496         final int leftTabWidth = mLeftSlider.getTabWidth();
    497         final int rightTabWidth = mRightSlider.getTabWidth();
    498         final int leftTabHeight = mLeftSlider.getTabHeight();
    499         final int rightTabHeight = mRightSlider.getTabHeight();
    500         final int width;
    501         final int height;
    502         if (isHorizontal()) {
    503             width = Math.max(widthSpecSize, leftTabWidth + rightTabWidth);
    504             height = Math.max(leftTabHeight, rightTabHeight);
    505         } else {
    506             width = Math.max(leftTabWidth, rightTabHeight);
    507             height = Math.max(heightSpecSize, leftTabHeight + rightTabHeight);
    508         }
    509         setMeasuredDimension(width, height);
    510     }
    511 
    512     @Override
    513     public boolean onInterceptTouchEvent(MotionEvent event) {
    514         final int action = event.getAction();
    515         final float x = event.getX();
    516         final float y = event.getY();
    517 
    518         if (mAnimating) {
    519             return false;
    520         }
    521 
    522         View leftHandle = mLeftSlider.tab;
    523         leftHandle.getHitRect(mTmpRect);
    524         boolean leftHit = mTmpRect.contains((int) x, (int) y);
    525 
    526         View rightHandle = mRightSlider.tab;
    527         rightHandle.getHitRect(mTmpRect);
    528         boolean rightHit = mTmpRect.contains((int)x, (int) y);
    529 
    530         if (!mTracking && !(leftHit || rightHit)) {
    531             return false;
    532         }
    533 
    534         switch (action) {
    535             case MotionEvent.ACTION_DOWN: {
    536                 mTracking = true;
    537                 mTriggered = false;
    538                 vibrate(VIBRATE_SHORT);
    539                 if (leftHit) {
    540                     mCurrentSlider = mLeftSlider;
    541                     mOtherSlider = mRightSlider;
    542                     mThreshold = isHorizontal() ? THRESHOLD : 1.0f - THRESHOLD;
    543                     setGrabbedState(OnTriggerListener.LEFT_HANDLE);
    544                 } else {
    545                     mCurrentSlider = mRightSlider;
    546                     mOtherSlider = mLeftSlider;
    547                     mThreshold = isHorizontal() ? 1.0f - THRESHOLD : THRESHOLD;
    548                     setGrabbedState(OnTriggerListener.RIGHT_HANDLE);
    549                 }
    550                 mCurrentSlider.setState(Slider.STATE_PRESSED);
    551                 mCurrentSlider.showTarget();
    552                 mOtherSlider.hide();
    553                 break;
    554             }
    555         }
    556 
    557         return true;
    558     }
    559 
    560     /**
    561      * Reset the tabs to their original state and stop any existing animation.
    562      * Animate them back into place if animate is true.
    563      *
    564      * @param animate
    565      */
    566     public void reset(boolean animate) {
    567         mLeftSlider.reset(animate);
    568         mRightSlider.reset(animate);
    569         if (!animate) {
    570             mAnimating = false;
    571         }
    572     }
    573 
    574     @Override
    575     public void setVisibility(int visibility) {
    576         // Clear animations so sliders don't continue to animate when we show the widget again.
    577         if (visibility != getVisibility() && visibility == View.INVISIBLE) {
    578            reset(false);
    579         }
    580         super.setVisibility(visibility);
    581     }
    582 
    583     @Override
    584     public boolean onTouchEvent(MotionEvent event) {
    585         if (mTracking) {
    586             final int action = event.getAction();
    587             final float x = event.getX();
    588             final float y = event.getY();
    589 
    590             switch (action) {
    591                 case MotionEvent.ACTION_MOVE:
    592                     if (withinView(x, y, this) ) {
    593                         moveHandle(x, y);
    594                         float position = isHorizontal() ? x : y;
    595                         float target = mThreshold * (isHorizontal() ? getWidth() : getHeight());
    596                         boolean thresholdReached;
    597                         if (isHorizontal()) {
    598                             thresholdReached = mCurrentSlider == mLeftSlider ?
    599                                     position > target : position < target;
    600                         } else {
    601                             thresholdReached = mCurrentSlider == mLeftSlider ?
    602                                     position < target : position > target;
    603                         }
    604                         if (!mTriggered && thresholdReached) {
    605                             mTriggered = true;
    606                             mTracking = false;
    607                             mCurrentSlider.setState(Slider.STATE_ACTIVE);
    608                             boolean isLeft = mCurrentSlider == mLeftSlider;
    609                             dispatchTriggerEvent(isLeft ?
    610                                 OnTriggerListener.LEFT_HANDLE : OnTriggerListener.RIGHT_HANDLE);
    611 
    612                             startAnimating(isLeft ? mHoldLeftOnTransition : mHoldRightOnTransition);
    613                             setGrabbedState(OnTriggerListener.NO_HANDLE);
    614                         }
    615                         break;
    616                     }
    617                     // Intentionally fall through - we're outside tracking rectangle
    618 
    619                 case MotionEvent.ACTION_UP:
    620                 case MotionEvent.ACTION_CANCEL:
    621                     cancelGrab();
    622                     break;
    623             }
    624         }
    625 
    626         return mTracking || super.onTouchEvent(event);
    627     }
    628 
    629     private void cancelGrab() {
    630         mTracking = false;
    631         mTriggered = false;
    632         mOtherSlider.show(true);
    633         mCurrentSlider.reset(false);
    634         mCurrentSlider.hideTarget();
    635         mCurrentSlider = null;
    636         mOtherSlider = null;
    637         setGrabbedState(OnTriggerListener.NO_HANDLE);
    638     }
    639 
    640     void startAnimating(final boolean holdAfter) {
    641         mAnimating = true;
    642         final Animation trans1;
    643         final Animation trans2;
    644         final Slider slider = mCurrentSlider;
    645         final Slider other = mOtherSlider;
    646         final int dx;
    647         final int dy;
    648         if (isHorizontal()) {
    649             int right = slider.tab.getRight();
    650             int width = slider.tab.getWidth();
    651             int left = slider.tab.getLeft();
    652             int viewWidth = getWidth();
    653             int holdOffset = holdAfter ? 0 : width; // how much of tab to show at the end of anim
    654             dx =  slider == mRightSlider ? - (right + viewWidth - holdOffset)
    655                     : (viewWidth - left) + viewWidth - holdOffset;
    656             dy = 0;
    657         } else {
    658             int top = slider.tab.getTop();
    659             int bottom = slider.tab.getBottom();
    660             int height = slider.tab.getHeight();
    661             int viewHeight = getHeight();
    662             int holdOffset = holdAfter ? 0 : height; // how much of tab to show at end of anim
    663             dx = 0;
    664             dy =  slider == mRightSlider ? (top + viewHeight - holdOffset)
    665                     : - ((viewHeight - bottom) + viewHeight - holdOffset);
    666         }
    667         trans1 = new TranslateAnimation(0, dx, 0, dy);
    668         trans1.setDuration(ANIM_DURATION);
    669         trans1.setInterpolator(new LinearInterpolator());
    670         trans1.setFillAfter(true);
    671         trans2 = new TranslateAnimation(0, dx, 0, dy);
    672         trans2.setDuration(ANIM_DURATION);
    673         trans2.setInterpolator(new LinearInterpolator());
    674         trans2.setFillAfter(true);
    675 
    676         trans1.setAnimationListener(new AnimationListener() {
    677             public void onAnimationEnd(Animation animation) {
    678                 Animation anim;
    679                 if (holdAfter) {
    680                     anim = new TranslateAnimation(dx, dx, dy, dy);
    681                     anim.setDuration(1000); // plenty of time for transitions
    682                     mAnimating = false;
    683                 } else {
    684                     anim = new AlphaAnimation(0.5f, 1.0f);
    685                     anim.setDuration(ANIM_DURATION);
    686                     resetView();
    687                 }
    688                 anim.setAnimationListener(mAnimationDoneListener);
    689 
    690                 /* Animation can be the same for these since the animation just holds */
    691                 mLeftSlider.startAnimation(anim, anim);
    692                 mRightSlider.startAnimation(anim, anim);
    693             }
    694 
    695             public void onAnimationRepeat(Animation animation) {
    696 
    697             }
    698 
    699             public void onAnimationStart(Animation animation) {
    700 
    701             }
    702 
    703         });
    704 
    705         slider.hideTarget();
    706         slider.startAnimation(trans1, trans2);
    707     }
    708 
    709     private void onAnimationDone() {
    710         resetView();
    711         mAnimating = false;
    712     }
    713 
    714     private boolean withinView(final float x, final float y, final View view) {
    715         return isHorizontal() && y > - TRACKING_MARGIN && y < TRACKING_MARGIN + view.getHeight()
    716             || !isHorizontal() && x > -TRACKING_MARGIN && x < TRACKING_MARGIN + view.getWidth();
    717     }
    718 
    719     private boolean isHorizontal() {
    720         return mOrientation == HORIZONTAL;
    721     }
    722 
    723     private void resetView() {
    724         mLeftSlider.reset(false);
    725         mRightSlider.reset(false);
    726         // onLayout(true, getLeft(), getTop(), getLeft() + getWidth(), getTop() + getHeight());
    727     }
    728 
    729     @Override
    730     protected void onLayout(boolean changed, int l, int t, int r, int b) {
    731         if (!changed) return;
    732 
    733         // Center the widgets in the view
    734         mLeftSlider.layout(l, t, r, b, isHorizontal() ? Slider.ALIGN_LEFT : Slider.ALIGN_BOTTOM);
    735         mRightSlider.layout(l, t, r, b, isHorizontal() ? Slider.ALIGN_RIGHT : Slider.ALIGN_TOP);
    736     }
    737 
    738     private void moveHandle(float x, float y) {
    739         final View handle = mCurrentSlider.tab;
    740         final View content = mCurrentSlider.text;
    741         if (isHorizontal()) {
    742             int deltaX = (int) x - handle.getLeft() - (handle.getWidth() / 2);
    743             handle.offsetLeftAndRight(deltaX);
    744             content.offsetLeftAndRight(deltaX);
    745         } else {
    746             int deltaY = (int) y - handle.getTop() - (handle.getHeight() / 2);
    747             handle.offsetTopAndBottom(deltaY);
    748             content.offsetTopAndBottom(deltaY);
    749         }
    750         invalidate(); // TODO: be more conservative about what we're invalidating
    751     }
    752 
    753     /**
    754      * Sets the left handle icon to a given resource.
    755      *
    756      * The resource should refer to a Drawable object, or use 0 to remove
    757      * the icon.
    758      *
    759      * @param iconId the resource ID of the icon drawable
    760      * @param targetId the resource of the target drawable
    761      * @param barId the resource of the bar drawable (stateful)
    762      * @param tabId the resource of the
    763      */
    764     public void setLeftTabResources(int iconId, int targetId, int barId, int tabId) {
    765         mLeftSlider.setIcon(iconId);
    766         mLeftSlider.setTarget(targetId);
    767         mLeftSlider.setBarBackgroundResource(barId);
    768         mLeftSlider.setTabBackgroundResource(tabId);
    769         mLeftSlider.updateDrawableStates();
    770     }
    771 
    772     /**
    773      * Sets the left handle hint text to a given resource string.
    774      *
    775      * @param resId
    776      */
    777     public void setLeftHintText(int resId) {
    778         if (isHorizontal()) {
    779             mLeftSlider.setHintText(resId);
    780         }
    781     }
    782 
    783     /**
    784      * Sets the right handle icon to a given resource.
    785      *
    786      * The resource should refer to a Drawable object, or use 0 to remove
    787      * the icon.
    788      *
    789      * @param iconId the resource ID of the icon drawable
    790      * @param targetId the resource of the target drawable
    791      * @param barId the resource of the bar drawable (stateful)
    792      * @param tabId the resource of the
    793      */
    794     public void setRightTabResources(int iconId, int targetId, int barId, int tabId) {
    795         mRightSlider.setIcon(iconId);
    796         mRightSlider.setTarget(targetId);
    797         mRightSlider.setBarBackgroundResource(barId);
    798         mRightSlider.setTabBackgroundResource(tabId);
    799         mRightSlider.updateDrawableStates();
    800     }
    801 
    802     /**
    803      * Sets the left handle hint text to a given resource string.
    804      *
    805      * @param resId
    806      */
    807     public void setRightHintText(int resId) {
    808         if (isHorizontal()) {
    809             mRightSlider.setHintText(resId);
    810         }
    811     }
    812 
    813     public void setHoldAfterTrigger(boolean holdLeft, boolean holdRight) {
    814         mHoldLeftOnTransition = holdLeft;
    815         mHoldRightOnTransition = holdRight;
    816     }
    817 
    818     /**
    819      * Triggers haptic feedback.
    820      */
    821     private synchronized void vibrate(long duration) {
    822         final boolean hapticEnabled = Settings.System.getIntForUser(
    823                 mContext.getContentResolver(), Settings.System.HAPTIC_FEEDBACK_ENABLED, 1,
    824                 UserHandle.USER_CURRENT) != 0;
    825         if (hapticEnabled) {
    826             if (mVibrator == null) {
    827                 mVibrator = (android.os.Vibrator) getContext()
    828                         .getSystemService(Context.VIBRATOR_SERVICE);
    829             }
    830             mVibrator.vibrate(duration, VIBRATION_ATTRIBUTES);
    831         }
    832     }
    833 
    834     /**
    835      * Registers a callback to be invoked when the user triggers an event.
    836      *
    837      * @param listener the OnDialTriggerListener to attach to this view
    838      */
    839     public void setOnTriggerListener(OnTriggerListener listener) {
    840         mOnTriggerListener = listener;
    841     }
    842 
    843     /**
    844      * Dispatches a trigger event to listener. Ignored if a listener is not set.
    845      * @param whichHandle the handle that triggered the event.
    846      */
    847     private void dispatchTriggerEvent(int whichHandle) {
    848         vibrate(VIBRATE_LONG);
    849         if (mOnTriggerListener != null) {
    850             mOnTriggerListener.onTrigger(this, whichHandle);
    851         }
    852     }
    853 
    854     @Override
    855     protected void onVisibilityChanged(View changedView, int visibility) {
    856         super.onVisibilityChanged(changedView, visibility);
    857         // When visibility changes and the user has a tab selected, unselect it and
    858         // make sure their callback gets called.
    859         if (changedView == this && visibility != VISIBLE
    860                 && mGrabbedState != OnTriggerListener.NO_HANDLE) {
    861             cancelGrab();
    862         }
    863     }
    864 
    865     /**
    866      * Sets the current grabbed state, and dispatches a grabbed state change
    867      * event to our listener.
    868      */
    869     private void setGrabbedState(int newState) {
    870         if (newState != mGrabbedState) {
    871             mGrabbedState = newState;
    872             if (mOnTriggerListener != null) {
    873                 mOnTriggerListener.onGrabbedStateChange(this, mGrabbedState);
    874             }
    875         }
    876     }
    877 
    878     private void log(String msg) {
    879         Log.d(LOG_TAG, msg);
    880     }
    881 }
    882