content: /*
   * Copyright (C) 2009 The Android Open Source Project
   *
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   *
   *      http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  
  package com.android.internal.widget;
  
  import android.annotation.UnsupportedAppUsage;
  import android.content.Context;
  import android.content.res.Resources;
  import android.content.res.TypedArray;
  import android.graphics.Rect;
  import android.graphics.drawable.Drawable;
  import android.media.AudioAttributes;
  import android.os.UserHandle;
  import android.os.Vibrator;
  import android.provider.Settings;
  import android.util.AttributeSet;
  import android.util.Log;
  import android.view.Gravity;
  import android.view.MotionEvent;
  import android.view.View;
  import android.view.ViewGroup;
  import android.view.animation.AlphaAnimation;
  import android.view.animation.Animation;
  import android.view.animation.LinearInterpolator;
  import android.view.animation.TranslateAnimation;
  import android.view.animation.Animation.AnimationListener;
  import android.widget.ImageView;
  import android.widget.TextView;
  import android.widget.ImageView.ScaleType;
  
  import com.android.internal.R;
  
  /**
   * A special widget containing two Sliders and a threshold for each.  Moving either slider beyond
   * the threshold will cause the registered OnTriggerListener.onTrigger() to be called with
   * whichHandle being {@link OnTriggerListener#LEFT_HANDLE} or {@link OnTriggerListener#RIGHT_HANDLE}
   * Equivalently, selecting a tab will result in a call to
   * {@link OnTriggerListener#onGrabbedStateChange(View, int)} with one of these two states. Releasing
   * the tab will result in whichHandle being {@link OnTriggerListener#NO_HANDLE}.
   *
   */
  public class SlidingTab extends ViewGroup {
      private static final String LOG_TAG = "SlidingTab";
      private static final boolean DBG = false;
      private static final int HORIZONTAL = 0; // as defined in attrs.xml
      private static final int VERTICAL = 1;
  
      // TODO: Make these configurable
      private static final float THRESHOLD = 2.0f / 3.0f;
      private static final long VIBRATE_SHORT = 30;
      private static final long VIBRATE_LONG = 40;
      private static final int TRACKING_MARGIN = 50;
      private static final int ANIM_DURATION = 250; // Time for most animations (in ms)
      private static final int ANIM_TARGET_TIME = 500; // Time to show targets (in ms)
      private boolean mHoldLeftOnTransition = true;
      private boolean mHoldRightOnTransition = true;
  
      private static final AudioAttributes VIBRATION_ATTRIBUTES = new AudioAttributes.Builder()
              .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
              .setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION)
              .build();
  
      private OnTriggerListener mOnTriggerListener;
      private int mGrabbedState = OnTriggerListener.NO_HANDLE;
      private boolean mTriggered = false;
      private Vibrator mVibrator;
      private final float mDensity; // used to scale dimensions for bitmaps.
  
      /**
       * Either {@link #HORIZONTAL} or {@link #VERTICAL}.
       */
      private final int mOrientation;
  
      @UnsupportedAppUsage
      private final Slider mLeftSlider;
      @UnsupportedAppUsage
      private final Slider mRightSlider;
      private Slider mCurrentSlider;
      private boolean mTracking;
      private float mThreshold;
      private Slider mOtherSlider;
      private boolean mAnimating;
      private final Rect mTmpRect;
  
      /**
       * Listener used to reset the view when the current animation completes.
       */
      @UnsupportedAppUsage
      private final AnimationListener mAnimationDoneListener = new AnimationListener() {
          public void onAnimationStart(Animation animation) {
  
          }
  
          public void onAnimationRepeat(Animation animation) {
  
          }
  
          public void onAnimationEnd(Animation animation) {
              onAnimationDone();
          }
      };
  
      /**
       * Interface definition for a callback to be invoked when a tab is triggered
       * by moving it beyond a threshold.
       */
      public interface OnTriggerListener {
          /**
           * The interface was triggered because the user let go of the handle without reaching the
           * threshold.
           */
          public static final int NO_HANDLE = 0;
  
          /**
           * The interface was triggered because the user grabbed the left handle and moved it past
           * the threshold.
           */
          public static final int LEFT_HANDLE = 1;
  
          /**
           * The interface was triggered because the user grabbed the right handle and moved it past
           * the threshold.
           */
          public static final int RIGHT_HANDLE = 2;
  
          /**
           * Called when the user moves a handle beyond the threshold.
           *
           * @param v The view that was triggered.
           * @param whichHandle  Which "dial handle" the user grabbed,
           *        either {@link #LEFT_HANDLE}, {@link #RIGHT_HANDLE}.
           */
          void onTrigger(View v, int whichHandle);
  
          /**
           * Called when the "grabbed state" changes (i.e. when the user either grabs or releases
           * one of the handles.)
           *
           * @param v the view that was triggered
           * @param grabbedState the new state: {@link #NO_HANDLE}, {@link #LEFT_HANDLE},
           * or {@link #RIGHT_HANDLE}.
           */
          void onGrabbedStateChange(View v, int grabbedState);
      }
  
      /**
       * Simple container class for all things pertinent to a slider.
       * A slider consists of 3 Views:
       *
       * {@link #tab} is the tab shown on the screen in the default state.
       * {@link #text} is the view revealed as the user slides the tab out.
       * {@link #target} is the target the user must drag the slider past to trigger the slider.
       *
       */
      private static class Slider {
          /**
           * Tab alignment - determines which side the tab should be drawn on
           */
          public static final int ALIGN_LEFT = 0;
          public static final int ALIGN_RIGHT = 1;
          public static final int ALIGN_TOP = 2;
          public static final int ALIGN_BOTTOM = 3;
          public static final int ALIGN_UNKNOWN = 4;
  
          /**
           * States for the view.
           */
          private static final int STATE_NORMAL = 0;
          private static final int STATE_PRESSED = 1;
          private static final int STATE_ACTIVE = 2;
  
          @UnsupportedAppUsage
          private final ImageView tab;
          @UnsupportedAppUsage
          private final TextView text;
          private final ImageView target;
          private int currentState = STATE_NORMAL;
          private int alignment = ALIGN_UNKNOWN;
          private int alignment_value;
  
          /**
           * Constructor
           *
           * @param parent the container view of this one
           * @param tabId drawable for the tab
           * @param barId drawable for the bar
           * @param targetId drawable for the target
           */
          Slider(ViewGroup parent, int tabId, int barId, int targetId) {
              // Create tab
              tab = new ImageView(parent.getContext());
              tab.setBackgroundResource(tabId);
              tab.setScaleType(ScaleType.CENTER);
              tab.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT,
                      LayoutParams.WRAP_CONTENT));
  
              // Create hint TextView
              text = new TextView(parent.getContext());
              text.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT,
                      LayoutParams.MATCH_PARENT));
              text.setBackgroundResource(barId);
              text.setTextAppearance(parent.getContext(), R.style.TextAppearance_SlidingTabNormal);
              // hint.setSingleLine();  // Hmm.. this causes the text to disappear off-screen
  
              // Create target
              target = new ImageView(parent.getContext());
              target.setImageResource(targetId);
              target.setScaleType(ScaleType.CENTER);
              target.setLayoutParams(
                      new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));
              target.setVisibility(View.INVISIBLE);
  
              parent.addView(target); // this needs to be first - relies on painter's algorithm
              parent.addView(tab);
              parent.addView(text);
          }
  
          void setIcon(int iconId) {
              tab.setImageResource(iconId);
          }
  
          void setTabBackgroundResource(int tabId) {
              tab.setBackgroundResource(tabId);
          }
  
          void setBarBackgroundResource(int barId) {
              text.setBackgroundResource(barId);
          }
  
          void setHintText(int resId) {
              text.setText(resId);
          }
  
          void hide() {
              boolean horiz = alignment == ALIGN_LEFT || alignment == ALIGN_RIGHT;
              int dx = horiz ? (alignment == ALIGN_LEFT ? alignment_value - tab.getRight()
                      : alignment_value - tab.getLeft()) : 0;
              int dy = horiz ? 0 : (alignment == ALIGN_TOP ? alignment_value - tab.getBottom()
                      : alignment_value - tab.getTop());
  
              Animation trans = new TranslateAnimation(0, dx, 0, dy);
              trans.setDuration(ANIM_DURATION);
              trans.setFillAfter(true);
              tab.startAnimation(trans);
              text.startAnimation(trans);
              target.setVisibility(View.INVISIBLE);
          }
  
          void show(boolean animate) {
              text.setVisibility(View.VISIBLE);
              tab.setVisibility(View.VISIBLE);
              //target.setVisibility(View.INVISIBLE);
              if (animate) {
                  boolean horiz = alignment == ALIGN_LEFT || alignment == ALIGN_RIGHT;
                  int dx = horiz ? (alignment == ALIGN_LEFT ? tab.getWidth() : -tab.getWidth()) : 0;
                  int dy = horiz ? 0: (alignment == ALIGN_TOP ? tab.getHeight() : -tab.getHeight());
  
                  Animation trans = new TranslateAnimation(-dx, 0, -dy, 0);
                  trans.setDuration(ANIM_DURATION);
                  tab.startAnimation(trans);
                  text.startAnimation(trans);
              }
          }
  
          void setState(int state) {
              text.setPressed(state == STATE_PRESSED);
              tab.setPressed(state == STATE_PRESSED);
              if (state == STATE_ACTIVE) {
                  final int[] activeState = new int[] {com.android.internal.R.attr.state_active};
                  if (text.getBackground().isStateful()) {
                      text.getBackground().setState(activeState);
                  }
                  if (tab.getBackground().isStateful()) {
                      tab.getBackground().setState(activeState);
                  }
                  text.setTextAppearance(text.getContext(), R.style.TextAppearance_SlidingTabActive);
              } else {
                  text.setTextAppearance(text.getContext(), R.style.TextAppearance_SlidingTabNormal);
              }
              currentState = state;
          }
  
          void showTarget() {
              AlphaAnimation alphaAnim = new AlphaAnimation(0.0f, 1.0f);
              alphaAnim.setDuration(ANIM_TARGET_TIME);
              target.startAnimation(alphaAnim);
              target.setVisibility(View.VISIBLE);
          }
  
          void reset(boolean animate) {
              setState(STATE_NORMAL);
              text.setVisibility(View.VISIBLE);
              text.setTextAppearance(text.getContext(), R.style.TextAppearance_SlidingTabNormal);
              tab.setVisibility(View.VISIBLE);
              target.setVisibility(View.INVISIBLE);
              final boolean horiz = alignment == ALIGN_LEFT || alignment == ALIGN_RIGHT;
              int dx = horiz ? (alignment == ALIGN_LEFT ?  alignment_value - tab.getLeft()
                      : alignment_value - tab.getRight()) : 0;
              int dy = horiz ? 0 : (alignment == ALIGN_TOP ? alignment_value - tab.getTop()
                      : alignment_value - tab.getBottom());
              if (animate) {
                  TranslateAnimation trans = new TranslateAnimation(0, dx, 0, dy);
                  trans.setDuration(ANIM_DURATION);
                  trans.setFillAfter(false);
                  text.startAnimation(trans);
                  tab.startAnimation(trans);
              } else {
                  if (horiz) {
                      text.offsetLeftAndRight(dx);
                      tab.offsetLeftAndRight(dx);
                  } else {
                      text.offsetTopAndBottom(dy);
                      tab.offsetTopAndBottom(dy);
                  }
                  text.clearAnimation();
                  tab.clearAnimation();
                  target.clearAnimation();
              }
          }
  
          void setTarget(int targetId) {
              target.setImageResource(targetId);
          }
  
          /**
           * Layout the given widgets within the parent.
           *
           * @param l the parent's left border
           * @param t the parent's top border
           * @param r the parent's right border
           * @param b the parent's bottom border
           * @param alignment which side to align the widget to
           */
          void layout(int l, int t, int r, int b, int alignment) {
              this.alignment = alignment;
              final Drawable tabBackground = tab.getBackground();
              final int handleWidth = tabBackground.getIntrinsicWidth();
              final int handleHeight = tabBackground.getIntrinsicHeight();
              final Drawable targetDrawable = target.getDrawable();
              final int targetWidth = targetDrawable.getIntrinsicWidth();
              final int targetHeight = targetDrawable.getIntrinsicHeight();
              final int parentWidth = r - l;
              final int parentHeight = b - t;
  
              final int leftTarget = (int) (THRESHOLD * parentWidth) - targetWidth + handleWidth / 2;
              final int rightTarget = (int) ((1.0f - THRESHOLD) * parentWidth) - handleWidth / 2;
              final int left = (parentWidth - handleWidth) / 2;
              final int right = left + handleWidth;
  
              if (alignment == ALIGN_LEFT || alignment == ALIGN_RIGHT) {
                  // horizontal
                  final int targetTop = (parentHeight - targetHeight) / 2;
                  final int targetBottom = targetTop + targetHeight;
                  final int top = (parentHeight - handleHeight) / 2;
                  final int bottom = (parentHeight + handleHeight) / 2;
                  if (alignment == ALIGN_LEFT) {
                      tab.layout(0, top, handleWidth, bottom);
                      text.layout(0 - parentWidth, top, 0, bottom);
                      text.setGravity(Gravity.RIGHT);
                      target.layout(leftTarget, targetTop, leftTarget + targetWidth, targetBottom);
                      alignment_value = l;
                  } else {
                      tab.layout(parentWidth - handleWidth, top, parentWidth, bottom);
                      text.layout(parentWidth, top, parentWidth + parentWidth, bottom);
                      target.layout(rightTarget, targetTop, rightTarget + targetWidth, targetBottom);
                      text.setGravity(Gravity.TOP);
                      alignment_value = r;
                  }
              } else {
                  // vertical
                  final int targetLeft = (parentWidth - targetWidth) / 2;
                  final int targetRight = (parentWidth + targetWidth) / 2;
                  final int top = (int) (THRESHOLD * parentHeight) + handleHeight / 2 - targetHeight;
                  final int bottom = (int) ((1.0f - THRESHOLD) * parentHeight) - handleHeight / 2;
                  if (alignment == ALIGN_TOP) {
                      tab.layout(left, 0, right, handleHeight);
                      text.layout(left, 0 - parentHeight, right, 0);
                      target.layout(targetLeft, top, targetRight, top + targetHeight);
                      alignment_value = t;
                  } else {
                      tab.layout(left, parentHeight - handleHeight, right, parentHeight);
                      text.layout(left, parentHeight, right, parentHeight + parentHeight);
                      target.layout(targetLeft, bottom, targetRight, bottom + targetHeight);
                      alignment_value = b;
                  }
              }
          }
  
          public void updateDrawableStates() {
              setState(currentState);
          }
  
          /**
           * Ensure all the dependent widgets are measured.
           */
          public void measure(int widthMeasureSpec, int heightMeasureSpec) {
              int width = MeasureSpec.getSize(widthMeasureSpec);
              int height = MeasureSpec.getSize(heightMeasureSpec);
              tab.measure(View.MeasureSpec.makeSafeMeasureSpec(width, View.MeasureSpec.UNSPECIFIED),
                      View.MeasureSpec.makeSafeMeasureSpec(height, View.MeasureSpec.UNSPECIFIED));
              text.measure(View.MeasureSpec.makeSafeMeasureSpec(width, View.MeasureSpec.UNSPECIFIED),
                      View.MeasureSpec.makeSafeMeasureSpec(height, View.MeasureSpec.UNSPECIFIED));
          }
  
          /**
           * Get the measured tab width. Must be called after {@link Slider#measure()}.
           * @return
           */
          public int getTabWidth() {
              return tab.getMeasuredWidth();
          }
  
          /**
           * Get the measured tab width. Must be called after {@link Slider#measure()}.
           * @return
           */
          public int getTabHeight() {
              return tab.getMeasuredHeight();
          }
  
          /**
           * Start animating the slider. Note we need two animations since a ValueAnimator
           * keeps internal state of the invalidation region which is just the view being animated.
           *
           * @param anim1
           * @param anim2
           */
          public void startAnimation(Animation anim1, Animation anim2) {
              tab.startAnimation(anim1);
              text.startAnimation(anim2);
          }
  
          public void hideTarget() {
              target.clearAnimation();
              target.setVisibility(View.INVISIBLE);
          }
      }
  
      public SlidingTab(Context context) {
          this(context, null);
      }
  
      /**
       * Constructor used when this widget is created from a layout file.
       */
      public SlidingTab(Context context, AttributeSet attrs) {
          super(context, attrs);
  
          // Allocate a temporary once that can be used everywhere.
          mTmpRect = new Rect();
  
          TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SlidingTab);
          mOrientation = a.getInt(R.styleable.SlidingTab_orientation, HORIZONTAL);
          a.recycle();
  
          Resources r = getResources();
          mDensity = r.getDisplayMetrics().density;
          if (DBG) log("- Density: " + mDensity);
  
          mLeftSlider = new Slider(this,
                  R.drawable.jog_tab_left_generic,
                  R.drawable.jog_tab_bar_left_generic,
                  R.drawable.jog_tab_target_gray);
          mRightSlider = new Slider(this,
                  R.drawable.jog_tab_right_generic,
                  R.drawable.jog_tab_bar_right_generic,
                  R.drawable.jog_tab_target_gray);
  
          // setBackgroundColor(0x80808080);
      }
  
      @Override
      protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
          int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
          int widthSpecSize =  MeasureSpec.getSize(widthMeasureSpec);
  
          int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
          int heightSpecSize =  MeasureSpec.getSize(heightMeasureSpec);
  
          if (DBG) {
              if (widthSpecMode == MeasureSpec.UNSPECIFIED 
                      || heightSpecMode == MeasureSpec.UNSPECIFIED) {
                  Log.e("SlidingTab", "SlidingTab cannot have UNSPECIFIED MeasureSpec"
                          +"(wspec=" + widthSpecMode + ", hspec=" + heightSpecMode + ")",
                          new RuntimeException(LOG_TAG + "stack:"));
              }
          }
  
          mLeftSlider.measure(widthMeasureSpec, heightMeasureSpec);
          mRightSlider.measure(widthMeasureSpec, heightMeasureSpec);
          final int leftTabWidth = mLeftSlider.getTabWidth();
          final int rightTabWidth = mRightSlider.getTabWidth();
          final int leftTabHeight = mLeftSlider.getTabHeight();
          final int rightTabHeight = mRightSlider.getTabHeight();
          final int width;
          final int height;
          if (isHorizontal()) {
              width = Math.max(widthSpecSize, leftTabWidth + rightTabWidth);
              height = Math.max(leftTabHeight, rightTabHeight);
          } else {
              width = Math.max(leftTabWidth, rightTabHeight);
              height = Math.max(heightSpecSize, leftTabHeight + rightTabHeight);
          }
          setMeasuredDimension(width, height);
      }
  
      @Override
      public boolean onInterceptTouchEvent(MotionEvent event) {
          final int action = event.getAction();
          final float x = event.getX();
          final float y = event.getY();
  
          if (mAnimating) {
              return false;
          }
  
          View leftHandle = mLeftSlider.tab;
          leftHandle.getHitRect(mTmpRect);
          boolean leftHit = mTmpRect.contains((int) x, (int) y);
  
          View rightHandle = mRightSlider.tab;
          rightHandle.getHitRect(mTmpRect);
          boolean rightHit = mTmpRect.contains((int)x, (int) y);
  
          if (!mTracking && !(leftHit || rightHit)) {
              return false;
          }
  
          switch (action) {
              case MotionEvent.ACTION_DOWN: {
                  mTracking = true;
                  mTriggered = false;
                  vibrate(VIBRATE_SHORT);
                  if (leftHit) {
                      mCurrentSlider = mLeftSlider;
                      mOtherSlider = mRightSlider;
                      mThreshold = isHorizontal() ? THRESHOLD : 1.0f - THRESHOLD;
                      setGrabbedState(OnTriggerListener.LEFT_HANDLE);
                  } else {
                      mCurrentSlider = mRightSlider;
                      mOtherSlider = mLeftSlider;
                      mThreshold = isHorizontal() ? 1.0f - THRESHOLD : THRESHOLD;
                      setGrabbedState(OnTriggerListener.RIGHT_HANDLE);
                  }
                  mCurrentSlider.setState(Slider.STATE_PRESSED);
                  mCurrentSlider.showTarget();
                  mOtherSlider.hide();
                  break;
              }
          }
  
          return true;
      }
  
      /**
       * Reset the tabs to their original state and stop any existing animation.
       * Animate them back into place if animate is true.
       *
       * @param animate
       */
      public void reset(boolean animate) {
          mLeftSlider.reset(animate);
          mRightSlider.reset(animate);
          if (!animate) {
              mAnimating = false;
          }
      }
  
      @Override
      public void setVisibility(int visibility) {
          // Clear animations so sliders don't continue to animate when we show the widget again.
          if (visibility != getVisibility() && visibility == View.INVISIBLE) {
             reset(false);
          }
          super.setVisibility(visibility);
      }
  
      @Override
      public boolean onTouchEvent(MotionEvent event) {
          if (mTracking) {
              final int action = event.getAction();
              final float x = event.getX();
              final float y = event.getY();
  
              switch (action) {
                  case MotionEvent.ACTION_MOVE:
                      if (withinView(x, y, this) ) {
                          moveHandle(x, y);
                          float position = isHorizontal() ? x : y;
                          float target = mThreshold * (isHorizontal() ? getWidth() : getHeight());
                          boolean thresholdReached;
                          if (isHorizontal()) {
                              thresholdReached = mCurrentSlider == mLeftSlider ?
                                      position > target : position < target;
                          } else {
                              thresholdReached = mCurrentSlider == mLeftSlider ?
                                      position < target : position > target;
                          }
                          if (!mTriggered && thresholdReached) {
                              mTriggered = true;
                              mTracking = false;
                              mCurrentSlider.setState(Slider.STATE_ACTIVE);
                              boolean isLeft = mCurrentSlider == mLeftSlider;
                              dispatchTriggerEvent(isLeft ?
                                  OnTriggerListener.LEFT_HANDLE : OnTriggerListener.RIGHT_HANDLE);
  
                              startAnimating(isLeft ? mHoldLeftOnTransition : mHoldRightOnTransition);
                              setGrabbedState(OnTriggerListener.NO_HANDLE);
                          }
                          break;
                      }
                      // Intentionally fall through - we're outside tracking rectangle
  
                  case MotionEvent.ACTION_UP:
                  case MotionEvent.ACTION_CANCEL:
                      cancelGrab();
                      break;
              }
          }
  
          return mTracking || super.onTouchEvent(event);
      }
  
      private void cancelGrab() {
          mTracking = false;
          mTriggered = false;
          mOtherSlider.show(true);
          mCurrentSlider.reset(false);
          mCurrentSlider.hideTarget();
          mCurrentSlider = null;
          mOtherSlider = null;
          setGrabbedState(OnTriggerListener.NO_HANDLE);
      }
  
      void startAnimating(final boolean holdAfter) {
          mAnimating = true;
          final Animation trans1;
          final Animation trans2;
          final Slider slider = mCurrentSlider;
          final Slider other = mOtherSlider;
          final int dx;
          final int dy;
          if (isHorizontal()) {
              int right = slider.tab.getRight();
              int width = slider.tab.getWidth();
              int left = slider.tab.getLeft();
              int viewWidth = getWidth();
              int holdOffset = holdAfter ? 0 : width; // how much of tab to show at the end of anim
              dx =  slider == mRightSlider ? - (right + viewWidth - holdOffset)
                      : (viewWidth - left) + viewWidth - holdOffset;
              dy = 0;
          } else {
              int top = slider.tab.getTop();
              int bottom = slider.tab.getBottom();
              int height = slider.tab.getHeight();
              int viewHeight = getHeight();
              int holdOffset = holdAfter ? 0 : height; // how much of tab to show at end of anim
              dx = 0;
              dy =  slider == mRightSlider ? (top + viewHeight - holdOffset)
                      : - ((viewHeight - bottom) + viewHeight - holdOffset);
          }
          trans1 = new TranslateAnimation(0, dx, 0, dy);
          trans1.setDuration(ANIM_DURATION);
          trans1.setInterpolator(new LinearInterpolator());
          trans1.setFillAfter(true);
          trans2 = new TranslateAnimation(0, dx, 0, dy);
          trans2.setDuration(ANIM_DURATION);
          trans2.setInterpolator(new LinearInterpolator());
          trans2.setFillAfter(true);
  
          trans1.setAnimationListener(new AnimationListener() {
              public void onAnimationEnd(Animation animation) {
                  Animation anim;
                  if (holdAfter) {
                      anim = new TranslateAnimation(dx, dx, dy, dy);
                      anim.setDuration(1000); // plenty of time for transitions
                      mAnimating = false;
                  } else {
                      anim = new AlphaAnimation(0.5f, 1.0f);
                      anim.setDuration(ANIM_DURATION);
                      resetView();
                  }
                  anim.setAnimationListener(mAnimationDoneListener);
  
                  /* Animation can be the same for these since the animation just holds */
                  mLeftSlider.startAnimation(anim, anim);
                  mRightSlider.startAnimation(anim, anim);
              }
  
              public void onAnimationRepeat(Animation animation) {
  
              }
  
              public void onAnimationStart(Animation animation) {
  
              }
  
          });
  
          slider.hideTarget();
          slider.startAnimation(trans1, trans2);
      }
  
      @UnsupportedAppUsage
      private void onAnimationDone() {
          resetView();
          mAnimating = false;
      }
  
      private boolean withinView(final float x, final float y, final View view) {
          return isHorizontal() && y > - TRACKING_MARGIN && y < TRACKING_MARGIN + view.getHeight()
              || !isHorizontal() && x > -TRACKING_MARGIN && x < TRACKING_MARGIN + view.getWidth();
      }
  
      private boolean isHorizontal() {
          return mOrientation == HORIZONTAL;
      }
  
      @UnsupportedAppUsage
      private void resetView() {
          mLeftSlider.reset(false);
          mRightSlider.reset(false);
          // onLayout(true, getLeft(), getTop(), getLeft() + getWidth(), getTop() + getHeight());
      }
  
      @Override
      protected void onLayout(boolean changed, int l, int t, int r, int b) {
          if (!changed) return;
  
          // Center the widgets in the view
          mLeftSlider.layout(l, t, r, b, isHorizontal() ? Slider.ALIGN_LEFT : Slider.ALIGN_BOTTOM);
          mRightSlider.layout(l, t, r, b, isHorizontal() ? Slider.ALIGN_RIGHT : Slider.ALIGN_TOP);
      }
  
      private void moveHandle(float x, float y) {
          final View handle = mCurrentSlider.tab;
          final View content = mCurrentSlider.text;
          if (isHorizontal()) {
              int deltaX = (int) x - handle.getLeft() - (handle.getWidth() / 2);
              handle.offsetLeftAndRight(deltaX);
              content.offsetLeftAndRight(deltaX);
          } else {
              int deltaY = (int) y - handle.getTop() - (handle.getHeight() / 2);
              handle.offsetTopAndBottom(deltaY);
              content.offsetTopAndBottom(deltaY);
          }
          invalidate(); // TODO: be more conservative about what we're invalidating
      }
  
      /**
       * Sets the left handle icon to a given resource.
       *
       * The resource should refer to a Drawable object, or use 0 to remove
       * the icon.
       *
       * @param iconId the resource ID of the icon drawable
       * @param targetId the resource of the target drawable
       * @param barId the resource of the bar drawable (stateful)
       * @param tabId the resource of the
       */
      @UnsupportedAppUsage
      public void setLeftTabResources(int iconId, int targetId, int barId, int tabId) {
          mLeftSlider.setIcon(iconId);
          mLeftSlider.setTarget(targetId);
          mLeftSlider.setBarBackgroundResource(barId);
          mLeftSlider.setTabBackgroundResource(tabId);
          mLeftSlider.updateDrawableStates();
      }
  
      /**
       * Sets the left handle hint text to a given resource string.
       *
       * @param resId
       */
      @UnsupportedAppUsage
      public void setLeftHintText(int resId) {
          if (isHorizontal()) {
              mLeftSlider.setHintText(resId);
          }
      }
  
      /**
       * Sets the right handle icon to a given resource.
       *
       * The resource should refer to a Drawable object, or use 0 to remove
       * the icon.
       *
       * @param iconId the resource ID of the icon drawable
       * @param targetId the resource of the target drawable
       * @param barId the resource of the bar drawable (stateful)
       * @param tabId the resource of the
       */
      @UnsupportedAppUsage
      public void setRightTabResources(int iconId, int targetId, int barId, int tabId) {
          mRightSlider.setIcon(iconId);
          mRightSlider.setTarget(targetId);
          mRightSlider.setBarBackgroundResource(barId);
          mRightSlider.setTabBackgroundResource(tabId);
          mRightSlider.updateDrawableStates();
      }
  
      /**
       * Sets the left handle hint text to a given resource string.
       *
       * @param resId
       */
      @UnsupportedAppUsage
      public void setRightHintText(int resId) {
          if (isHorizontal()) {
              mRightSlider.setHintText(resId);
          }
      }
  
      @UnsupportedAppUsage
      public void setHoldAfterTrigger(boolean holdLeft, boolean holdRight) {
          mHoldLeftOnTransition = holdLeft;
          mHoldRightOnTransition = holdRight;
      }
  
      /**
       * Triggers haptic feedback.
       */
      private synchronized void vibrate(long duration) {
          final boolean hapticEnabled = Settings.System.getIntForUser(
                  mContext.getContentResolver(), Settings.System.HAPTIC_FEEDBACK_ENABLED, 1,
                  UserHandle.USER_CURRENT) != 0;
          if (hapticEnabled) {
              if (mVibrator == null) {
                  mVibrator = (android.os.Vibrator) getContext()
                          .getSystemService(Context.VIBRATOR_SERVICE);
              }
              mVibrator.vibrate(duration, VIBRATION_ATTRIBUTES);
          }
      }
  
      /**
       * Registers a callback to be invoked when the user triggers an event.
       *
       * @param listener the OnDialTriggerListener to attach to this view
       */
      @UnsupportedAppUsage
      public void setOnTriggerListener(OnTriggerListener listener) {
          mOnTriggerListener = listener;
      }
  
      /**
       * Dispatches a trigger event to listener. Ignored if a listener is not set.
       * @param whichHandle the handle that triggered the event.
       */
      private void dispatchTriggerEvent(int whichHandle) {
          vibrate(VIBRATE_LONG);
          if (mOnTriggerListener != null) {
              mOnTriggerListener.onTrigger(this, whichHandle);
          }
      }
  
      @Override
      protected void onVisibilityChanged(View changedView, int visibility) {
          super.onVisibilityChanged(changedView, visibility);
          // When visibility changes and the user has a tab selected, unselect it and
          // make sure their callback gets called.
          if (changedView == this && visibility != VISIBLE
                  && mGrabbedState != OnTriggerListener.NO_HANDLE) {
              cancelGrab();
          }
      }
  
      /**
       * Sets the current grabbed state, and dispatches a grabbed state change
       * event to our listener.
       */
      private void setGrabbedState(int newState) {
          if (newState != mGrabbedState) {
              mGrabbedState = newState;
              if (mOnTriggerListener != null) {
                  mOnTriggerListener.onGrabbedStateChange(this, mGrabbedState);
              }
          }
      }
  
      private void log(String msg) {
          Log.d(LOG_TAG, msg);
      }
  }
  