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 android.widget;
  
  import android.annotation.ColorInt;
  import android.annotation.NonNull;
  import android.annotation.UnsupportedAppUsage;
  import android.content.Context;
  import android.content.res.Configuration;
  import android.content.res.TypedArray;
  import android.graphics.Canvas;
  import android.graphics.Rect;
  import android.os.Build;
  import android.os.Bundle;
  import android.os.Parcel;
  import android.os.Parcelable;
  import android.util.AttributeSet;
  import android.util.Log;
  import android.view.FocusFinder;
  import android.view.InputDevice;
  import android.view.KeyEvent;
  import android.view.MotionEvent;
  import android.view.VelocityTracker;
  import android.view.View;
  import android.view.ViewConfiguration;
  import android.view.ViewDebug;
  import android.view.ViewGroup;
  import android.view.ViewHierarchyEncoder;
  import android.view.ViewParent;
  import android.view.accessibility.AccessibilityEvent;
  import android.view.accessibility.AccessibilityNodeInfo;
  import android.view.animation.AnimationUtils;
  import android.view.inspector.InspectableProperty;
  
  import com.android.internal.R;
  
  import java.util.List;
  
  /**
   * Layout container for a view hierarchy that can be scrolled by the user,
   * allowing it to be larger than the physical display.  A HorizontalScrollView
   * is a {@link FrameLayout}, meaning you should place one child in it
   * containing the entire contents to scroll; this child may itself be a layout
   * manager with a complex hierarchy of objects.  A child that is often used
   * is a {@link LinearLayout} in a horizontal orientation, presenting a horizontal
   * array of top-level items that the user can scroll through.
   *
   * <p>The {@link TextView} class also
   * takes care of its own scrolling, so does not require a HorizontalScrollView, but
   * using the two together is possible to achieve the effect of a text view
   * within a larger container.
   *
   * <p>HorizontalScrollView only supports horizontal scrolling. For vertical scrolling,
   * use either {@link ScrollView} or {@link ListView}.
   *
   * @attr ref android.R.styleable#HorizontalScrollView_fillViewport
   */
  public class HorizontalScrollView extends FrameLayout {
      private static final int ANIMATED_SCROLL_GAP = ScrollView.ANIMATED_SCROLL_GAP;
  
      private static final float MAX_SCROLL_FACTOR = ScrollView.MAX_SCROLL_FACTOR;
  
      private static final String TAG = "HorizontalScrollView";
  
      private long mLastScroll;
  
      private final Rect mTempRect = new Rect();
      @UnsupportedAppUsage
      private OverScroller mScroller;
      /**
       * Tracks the state of the left edge glow.
       *
       * Even though this field is practically final, we cannot make it final because there are apps
       * setting it via reflection and they need to keep working until they target Q.
       */
      @NonNull
      @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 124053130)
      private EdgeEffect mEdgeGlowLeft = new EdgeEffect(getContext());
  
      /**
       * Tracks the state of the bottom edge glow.
       *
       * Even though this field is practically final, we cannot make it final because there are apps
       * setting it via reflection and they need to keep working until they target Q.
       */
      @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 124052619)
      private EdgeEffect mEdgeGlowRight = new EdgeEffect(getContext());
  
      /**
       * Position of the last motion event.
       */
      @UnsupportedAppUsage
      private int mLastMotionX;
  
      /**
       * True when the layout has changed but the traversal has not come through yet.
       * Ideally the view hierarchy would keep track of this for us.
       */
      private boolean mIsLayoutDirty = true;
  
      /**
       * The child to give focus to in the event that a child has requested focus while the
       * layout is dirty. This prevents the scroll from being wrong if the child has not been
       * laid out before requesting focus.
       */
      @UnsupportedAppUsage
      private View mChildToScrollTo = null;
  
      /**
       * True if the user is currently dragging this ScrollView around. This is
       * not the same as 'is being flinged', which can be checked by
       * mScroller.isFinished() (flinging begins when the user lifts his finger).
       */
      @UnsupportedAppUsage
      private boolean mIsBeingDragged = false;
  
      /**
       * Determines speed during touch scrolling
       */
      @UnsupportedAppUsage
      private VelocityTracker mVelocityTracker;
  
      /**
       * When set to true, the scroll view measure its child to make it fill the currently
       * visible area.
       */
      @ViewDebug.ExportedProperty(category = "layout")
      private boolean mFillViewport;
  
      /**
       * Whether arrow scrolling is animated.
       */
      private boolean mSmoothScrollingEnabled = true;
  
      private int mTouchSlop;
      private int mMinimumVelocity;
      private int mMaximumVelocity;
  
      @UnsupportedAppUsage
      private int mOverscrollDistance;
      @UnsupportedAppUsage
      private int mOverflingDistance;
  
      private float mHorizontalScrollFactor;
  
      /**
       * ID of the active pointer. This is used to retain consistency during
       * drags/flings if multiple pointers are used.
       */
      private int mActivePointerId = INVALID_POINTER;
  
      /**
       * Sentinel value for no current active pointer.
       * Used by {@link #mActivePointerId}.
       */
      private static final int INVALID_POINTER = -1;
  
      private SavedState mSavedState;
  
      public HorizontalScrollView(Context context) {
          this(context, null);
      }
  
      public HorizontalScrollView(Context context, AttributeSet attrs) {
          this(context, attrs, com.android.internal.R.attr.horizontalScrollViewStyle);
      }
  
      public HorizontalScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
          this(context, attrs, defStyleAttr, 0);
      }
  
      public HorizontalScrollView(
              Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
          super(context, attrs, defStyleAttr, defStyleRes);
          initScrollView();
  
          final TypedArray a = context.obtainStyledAttributes(
                  attrs, android.R.styleable.HorizontalScrollView, defStyleAttr, defStyleRes);
          saveAttributeDataForStyleable(context, android.R.styleable.HorizontalScrollView,
                  attrs, a, defStyleAttr, defStyleRes);
  
          setFillViewport(a.getBoolean(android.R.styleable.HorizontalScrollView_fillViewport, false));
  
          a.recycle();
  
          if (context.getResources().getConfiguration().uiMode == Configuration.UI_MODE_TYPE_WATCH) {
              setRevealOnFocusHint(false);
          }
      }
  
      @Override
      protected float getLeftFadingEdgeStrength() {
          if (getChildCount() == 0) {
              return 0.0f;
          }
  
          final int length = getHorizontalFadingEdgeLength();
          if (mScrollX < length) {
              return mScrollX / (float) length;
          }
  
          return 1.0f;
      }
  
      @Override
      protected float getRightFadingEdgeStrength() {
          if (getChildCount() == 0) {
              return 0.0f;
          }
  
          final int length = getHorizontalFadingEdgeLength();
          final int rightEdge = getWidth() - mPaddingRight;
          final int span = getChildAt(0).getRight() - mScrollX - rightEdge;
          if (span < length) {
              return span / (float) length;
          }
  
          return 1.0f;
      }
  
      /**
       * Sets the edge effect color for both left and right edge effects.
       *
       * @param color The color for the edge effects.
       * @see #setLeftEdgeEffectColor(int)
       * @see #setRightEdgeEffectColor(int)
       * @see #getLeftEdgeEffectColor()
       * @see #getRightEdgeEffectColor()
       */
      public void setEdgeEffectColor(@ColorInt int color) {
          setLeftEdgeEffectColor(color);
          setRightEdgeEffectColor(color);
      }
  
      /**
       * Sets the right edge effect color.
       *
       * @param color The color for the right edge effect.
       * @see #setLeftEdgeEffectColor(int)
       * @see #setEdgeEffectColor(int)
       * @see #getLeftEdgeEffectColor()
       * @see #getRightEdgeEffectColor()
       */
      public void setRightEdgeEffectColor(@ColorInt int color) {
          mEdgeGlowRight.setColor(color);
      }
  
      /**
       * Sets the left edge effect color.
       *
       * @param color The color for the left edge effect.
       * @see #setRightEdgeEffectColor(int)
       * @see #setEdgeEffectColor(int)
       * @see #getLeftEdgeEffectColor()
       * @see #getRightEdgeEffectColor()
       */
      public void setLeftEdgeEffectColor(@ColorInt int color) {
          mEdgeGlowLeft.setColor(color);
      }
  
      /**
       * Returns the left edge effect color.
       *
       * @return The left edge effect color.
       * @see #setEdgeEffectColor(int)
       * @see #setLeftEdgeEffectColor(int)
       * @see #setRightEdgeEffectColor(int)
       * @see #getRightEdgeEffectColor()
       */
      @ColorInt
      public int getLeftEdgeEffectColor() {
          return mEdgeGlowLeft.getColor();
      }
  
      /**
       * Returns the right edge effect color.
       *
       * @return The right edge effect color.
       * @see #setEdgeEffectColor(int)
       * @see #setLeftEdgeEffectColor(int)
       * @see #setRightEdgeEffectColor(int)
       * @see #getLeftEdgeEffectColor()
       */
      @ColorInt
      public int getRightEdgeEffectColor() {
          return mEdgeGlowRight.getColor();
      }
  
      /**
       * @return The maximum amount this scroll view will scroll in response to
       *   an arrow event.
       */
      public int getMaxScrollAmount() {
          return (int) (MAX_SCROLL_FACTOR * (mRight - mLeft));
      }
  
  
      private void initScrollView() {
          mScroller = new OverScroller(getContext());
          setFocusable(true);
          setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
          setWillNotDraw(false);
          final ViewConfiguration configuration = ViewConfiguration.get(mContext);
          mTouchSlop = configuration.getScaledTouchSlop();
          mMinimumVelocity = configuration.getScaledMinimumFlingVelocity();
          mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
          mOverscrollDistance = configuration.getScaledOverscrollDistance();
          mOverflingDistance = configuration.getScaledOverflingDistance();
          mHorizontalScrollFactor = configuration.getScaledHorizontalScrollFactor();
      }
  
      @Override
      public void addView(View child) {
          if (getChildCount() > 0) {
              throw new IllegalStateException("HorizontalScrollView can host only one direct child");
          }
  
          super.addView(child);
      }
  
      @Override
      public void addView(View child, int index) {
          if (getChildCount() > 0) {
              throw new IllegalStateException("HorizontalScrollView can host only one direct child");
          }
  
          super.addView(child, index);
      }
  
      @Override
      public void addView(View child, ViewGroup.LayoutParams params) {
          if (getChildCount() > 0) {
              throw new IllegalStateException("HorizontalScrollView can host only one direct child");
          }
  
          super.addView(child, params);
      }
  
      @Override
      public void addView(View child, int index, ViewGroup.LayoutParams params) {
          if (getChildCount() > 0) {
              throw new IllegalStateException("HorizontalScrollView can host only one direct child");
          }
  
          super.addView(child, index, params);
      }
  
      /**
       * @return Returns true this HorizontalScrollView can be scrolled
       */
      private boolean canScroll() {
          View child = getChildAt(0);
          if (child != null) {
              int childWidth = child.getWidth();
              return getWidth() < childWidth + mPaddingLeft + mPaddingRight ;
          }
          return false;
      }
  
      /**
       * Indicates whether this HorizontalScrollView's content is stretched to
       * fill the viewport.
       *
       * @return True if the content fills the viewport, false otherwise.
       *
       * @attr ref android.R.styleable#HorizontalScrollView_fillViewport
       */
      @InspectableProperty
      public boolean isFillViewport() {
          return mFillViewport;
      }
  
      /**
       * Indicates this HorizontalScrollView whether it should stretch its content width
       * to fill the viewport or not.
       *
       * @param fillViewport True to stretch the content's width to the viewport's
       *        boundaries, false otherwise.
       *
       * @attr ref android.R.styleable#HorizontalScrollView_fillViewport
       */
      public void setFillViewport(boolean fillViewport) {
          if (fillViewport != mFillViewport) {
              mFillViewport = fillViewport;
              requestLayout();
          }
      }
  
      /**
       * @return Whether arrow scrolling will animate its transition.
       */
      public boolean isSmoothScrollingEnabled() {
          return mSmoothScrollingEnabled;
      }
  
      /**
       * Set whether arrow scrolling will animate its transition.
       * @param smoothScrollingEnabled whether arrow scrolling will animate its transition
       */
      public void setSmoothScrollingEnabled(boolean smoothScrollingEnabled) {
          mSmoothScrollingEnabled = smoothScrollingEnabled;
      }
  
      @Override
      protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
          super.onMeasure(widthMeasureSpec, heightMeasureSpec);
  
          if (!mFillViewport) {
              return;
          }
  
          final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
          if (widthMode == MeasureSpec.UNSPECIFIED) {
              return;
          }
  
          if (getChildCount() > 0) {
              final View child = getChildAt(0);
              final int widthPadding;
              final int heightPadding;
              final FrameLayout.LayoutParams lp = (LayoutParams) child.getLayoutParams();
              final int targetSdkVersion = getContext().getApplicationInfo().targetSdkVersion;
              if (targetSdkVersion >= Build.VERSION_CODES.M) {
                  widthPadding = mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin;
                  heightPadding = mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin;
              } else {
                  widthPadding = mPaddingLeft + mPaddingRight;
                  heightPadding = mPaddingTop + mPaddingBottom;
              }
  
              int desiredWidth = getMeasuredWidth() - widthPadding;
              if (child.getMeasuredWidth() < desiredWidth) {
                  final int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
                          desiredWidth, MeasureSpec.EXACTLY);
                  final int childHeightMeasureSpec = getChildMeasureSpec(
                          heightMeasureSpec, heightPadding, lp.height);
                  child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
              }
          }
      }
  
      @Override
      public boolean dispatchKeyEvent(KeyEvent event) {
          // Let the focused view and/or our descendants get the key first
          return super.dispatchKeyEvent(event) || executeKeyEvent(event);
      }
  
      /**
       * You can call this function yourself to have the scroll view perform
       * scrolling from a key event, just as if the event had been dispatched to
       * it by the view hierarchy.
       *
       * @param event The key event to execute.
       * @return Return true if the event was handled, else false.
       */
      public boolean executeKeyEvent(KeyEvent event) {
          mTempRect.setEmpty();
  
          if (!canScroll()) {
              if (isFocused()) {
                  View currentFocused = findFocus();
                  if (currentFocused == this) currentFocused = null;
                  View nextFocused = FocusFinder.getInstance().findNextFocus(this,
                          currentFocused, View.FOCUS_RIGHT);
                  return nextFocused != null && nextFocused != this &&
                          nextFocused.requestFocus(View.FOCUS_RIGHT);
              }
              return false;
          }
  
          boolean handled = false;
          if (event.getAction() == KeyEvent.ACTION_DOWN) {
              switch (event.getKeyCode()) {
                  case KeyEvent.KEYCODE_DPAD_LEFT:
                      if (!event.isAltPressed()) {
                          handled = arrowScroll(View.FOCUS_LEFT);
                      } else {
                          handled = fullScroll(View.FOCUS_LEFT);
                      }
                      break;
                  case KeyEvent.KEYCODE_DPAD_RIGHT:
                      if (!event.isAltPressed()) {
                          handled = arrowScroll(View.FOCUS_RIGHT);
                      } else {
                          handled = fullScroll(View.FOCUS_RIGHT);
                      }
                      break;
                  case KeyEvent.KEYCODE_SPACE:
                      pageScroll(event.isShiftPressed() ? View.FOCUS_LEFT : View.FOCUS_RIGHT);
                      break;
              }
          }
  
          return handled;
      }
  
      private boolean inChild(int x, int y) {
          if (getChildCount() > 0) {
              final int scrollX = mScrollX;
              final View child = getChildAt(0);
              return !(y < child.getTop()
                      || y >= child.getBottom()
                      || x < child.getLeft() - scrollX
                      || x >= child.getRight() - scrollX);
          }
          return false;
      }
  
      private void initOrResetVelocityTracker() {
          if (mVelocityTracker == null) {
              mVelocityTracker = VelocityTracker.obtain();
          } else {
              mVelocityTracker.clear();
          }
      }
  
      private void initVelocityTrackerIfNotExists() {
          if (mVelocityTracker == null) {
              mVelocityTracker = VelocityTracker.obtain();
          }
      }
  
      @UnsupportedAppUsage
      private void recycleVelocityTracker() {
          if (mVelocityTracker != null) {
              mVelocityTracker.recycle();
              mVelocityTracker = null;
          }
      }
  
      @Override
      public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
          if (disallowIntercept) {
              recycleVelocityTracker();
          }
          super.requestDisallowInterceptTouchEvent(disallowIntercept);
      }
  
      @Override
      public boolean onInterceptTouchEvent(MotionEvent ev) {
          /*
           * This method JUST determines whether we want to intercept the motion.
           * If we return true, onMotionEvent will be called and we do the actual
           * scrolling there.
           */
  
          /*
          * Shortcut the most recurring case: the user is in the dragging
          * state and he is moving his finger.  We want to intercept this
          * motion.
          */
          final int action = ev.getAction();
          if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) {
              return true;
          }
  
          if (super.onInterceptTouchEvent(ev)) {
              return true;
          }
  
          switch (action & MotionEvent.ACTION_MASK) {
              case MotionEvent.ACTION_MOVE: {
                  /*
                   * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check
                   * whether the user has moved far enough from his original down touch.
                   */
  
                  /*
                  * Locally do absolute value. mLastMotionX is set to the x value
                  * of the down event.
                  */
                  final int activePointerId = mActivePointerId;
                  if (activePointerId == INVALID_POINTER) {
                      // If we don't have a valid id, the touch down wasn't on content.
                      break;
                  }
  
                  final int pointerIndex = ev.findPointerIndex(activePointerId);
                  if (pointerIndex == -1) {
                      Log.e(TAG, "Invalid pointerId=" + activePointerId
                              + " in onInterceptTouchEvent");
                      break;
                  }
  
                  final int x = (int) ev.getX(pointerIndex);
                  final int xDiff = (int) Math.abs(x - mLastMotionX);
                  if (xDiff > mTouchSlop) {
                      mIsBeingDragged = true;
                      mLastMotionX = x;
                      initVelocityTrackerIfNotExists();
                      mVelocityTracker.addMovement(ev);
                      if (mParent != null) mParent.requestDisallowInterceptTouchEvent(true);
                  }
                  break;
              }
  
              case MotionEvent.ACTION_DOWN: {
                  final int x = (int) ev.getX();
                  if (!inChild((int) x, (int) ev.getY())) {
                      mIsBeingDragged = false;
                      recycleVelocityTracker();
                      break;
                  }
  
                  /*
                   * Remember location of down touch.
                   * ACTION_DOWN always refers to pointer index 0.
                   */
                  mLastMotionX = x;
                  mActivePointerId = ev.getPointerId(0);
  
                  initOrResetVelocityTracker();
                  mVelocityTracker.addMovement(ev);
  
                  /*
                  * If being flinged and user touches the screen, initiate drag;
                  * otherwise don't.  mScroller.isFinished should be false when
                  * being flinged.
                  */
                  mIsBeingDragged = !mScroller.isFinished();
                  break;
              }
  
              case MotionEvent.ACTION_CANCEL:
              case MotionEvent.ACTION_UP:
                  /* Release the drag */
                  mIsBeingDragged = false;
                  mActivePointerId = INVALID_POINTER;
                  if (mScroller.springBack(mScrollX, mScrollY, 0, getScrollRange(), 0, 0)) {
                      postInvalidateOnAnimation();
                  }
                  break;
              case MotionEvent.ACTION_POINTER_DOWN: {
                  final int index = ev.getActionIndex();
                  mLastMotionX = (int) ev.getX(index);
                  mActivePointerId = ev.getPointerId(index);
                  break;
              }
              case MotionEvent.ACTION_POINTER_UP:
                  onSecondaryPointerUp(ev);
                  mLastMotionX = (int) ev.getX(ev.findPointerIndex(mActivePointerId));
                  break;
          }
  
          /*
          * The only time we want to intercept motion events is if we are in the
          * drag mode.
          */
          return mIsBeingDragged;
      }
  
      @Override
      public boolean onTouchEvent(MotionEvent ev) {
          initVelocityTrackerIfNotExists();
          mVelocityTracker.addMovement(ev);
  
          final int action = ev.getAction();
  
          switch (action & MotionEvent.ACTION_MASK) {
              case MotionEvent.ACTION_DOWN: {
                  if (getChildCount() == 0) {
                      return false;
                  }
                  if ((mIsBeingDragged = !mScroller.isFinished())) {
                      final ViewParent parent = getParent();
                      if (parent != null) {
                          parent.requestDisallowInterceptTouchEvent(true);
                      }
                  }
  
                  /*
                   * If being flinged and user touches, stop the fling. isFinished
                   * will be false if being flinged.
                   */
                  if (!mScroller.isFinished()) {
                      mScroller.abortAnimation();
                  }
  
                  // Remember where the motion event started
                  mLastMotionX = (int) ev.getX();
                  mActivePointerId = ev.getPointerId(0);
                  break;
              }
              case MotionEvent.ACTION_MOVE:
                  final int activePointerIndex = ev.findPointerIndex(mActivePointerId);
                  if (activePointerIndex == -1) {
                      Log.e(TAG, "Invalid pointerId=" + mActivePointerId + " in onTouchEvent");
                      break;
                  }
  
                  final int x = (int) ev.getX(activePointerIndex);
                  int deltaX = mLastMotionX - x;
                  if (!mIsBeingDragged && Math.abs(deltaX) > mTouchSlop) {
                      final ViewParent parent = getParent();
                      if (parent != null) {
                          parent.requestDisallowInterceptTouchEvent(true);
                      }
                      mIsBeingDragged = true;
                      if (deltaX > 0) {
                          deltaX -= mTouchSlop;
                      } else {
                          deltaX += mTouchSlop;
                      }
                  }
                  if (mIsBeingDragged) {
                      // Scroll to follow the motion event
                      mLastMotionX = x;
  
                      final int oldX = mScrollX;
                      final int oldY = mScrollY;
                      final int range = getScrollRange();
                      final int overscrollMode = getOverScrollMode();
                      final boolean canOverscroll = overscrollMode == OVER_SCROLL_ALWAYS ||
                              (overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0);
  
                      // Calling overScrollBy will call onOverScrolled, which
                      // calls onScrollChanged if applicable.
                      if (overScrollBy(deltaX, 0, mScrollX, 0, range, 0,
                              mOverscrollDistance, 0, true)) {
                          // Break our velocity if we hit a scroll barrier.
                          mVelocityTracker.clear();
                      }
  
                      if (canOverscroll) {
                          final int pulledToX = oldX + deltaX;
                          if (pulledToX < 0) {
                              mEdgeGlowLeft.onPull((float) deltaX / getWidth(),
                                      1.f - ev.getY(activePointerIndex) / getHeight());
                              if (!mEdgeGlowRight.isFinished()) {
                                  mEdgeGlowRight.onRelease();
                              }
                          } else if (pulledToX > range) {
                              mEdgeGlowRight.onPull((float) deltaX / getWidth(),
                                      ev.getY(activePointerIndex) / getHeight());
                              if (!mEdgeGlowLeft.isFinished()) {
                                  mEdgeGlowLeft.onRelease();
                              }
                          }
                          if (shouldDisplayEdgeEffects()
                                  && (!mEdgeGlowLeft.isFinished() || !mEdgeGlowRight.isFinished())) {
                              postInvalidateOnAnimation();
                          }
                      }
                  }
                  break;
              case MotionEvent.ACTION_UP:
                  if (mIsBeingDragged) {
                      final VelocityTracker velocityTracker = mVelocityTracker;
                      velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
                      int initialVelocity = (int) velocityTracker.getXVelocity(mActivePointerId);
  
                      if (getChildCount() > 0) {
                          if ((Math.abs(initialVelocity) > mMinimumVelocity)) {
                              fling(-initialVelocity);
                          } else {
                              if (mScroller.springBack(mScrollX, mScrollY, 0,
                                      getScrollRange(), 0, 0)) {
                                  postInvalidateOnAnimation();
                              }
                          }
                      }
  
                      mActivePointerId = INVALID_POINTER;
                      mIsBeingDragged = false;
                      recycleVelocityTracker();
  
                      if (shouldDisplayEdgeEffects()) {
                          mEdgeGlowLeft.onRelease();
                          mEdgeGlowRight.onRelease();
                      }
                  }
                  break;
              case MotionEvent.ACTION_CANCEL:
                  if (mIsBeingDragged && getChildCount() > 0) {
                      if (mScroller.springBack(mScrollX, mScrollY, 0, getScrollRange(), 0, 0)) {
                          postInvalidateOnAnimation();
                      }
                      mActivePointerId = INVALID_POINTER;
                      mIsBeingDragged = false;
                      recycleVelocityTracker();
  
                      if (shouldDisplayEdgeEffects()) {
                          mEdgeGlowLeft.onRelease();
                          mEdgeGlowRight.onRelease();
                      }
                  }
                  break;
              case MotionEvent.ACTION_POINTER_UP:
                  onSecondaryPointerUp(ev);
                  break;
          }
          return true;
      }
  
      private void onSecondaryPointerUp(MotionEvent ev) {
          final int pointerIndex = (ev.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) >>
                  MotionEvent.ACTION_POINTER_INDEX_SHIFT;
          final int pointerId = ev.getPointerId(pointerIndex);
          if (pointerId == mActivePointerId) {
              // This was our active pointer going up. Choose a new
              // active pointer and adjust accordingly.
              // TODO: Make this decision more intelligent.
              final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
              mLastMotionX = (int) ev.getX(newPointerIndex);
              mActivePointerId = ev.getPointerId(newPointerIndex);
              if (mVelocityTracker != null) {
                  mVelocityTracker.clear();
              }
          }
      }
  
      @Override
      public boolean onGenericMotionEvent(MotionEvent event) {
          switch (event.getAction()) {
              case MotionEvent.ACTION_SCROLL: {
                  if (!mIsBeingDragged) {
                      final float axisValue;
                      if (event.isFromSource(InputDevice.SOURCE_CLASS_POINTER)) {
                          if ((event.getMetaState() & KeyEvent.META_SHIFT_ON) != 0) {
                              axisValue = -event.getAxisValue(MotionEvent.AXIS_VSCROLL);
                          } else {
                              axisValue = event.getAxisValue(MotionEvent.AXIS_HSCROLL);
                          }
                      } else if (event.isFromSource(InputDevice.SOURCE_ROTARY_ENCODER)) {
                          axisValue = event.getAxisValue(MotionEvent.AXIS_SCROLL);
                      } else {
                          axisValue = 0;
                      }
  
                      final int delta = Math.round(axisValue * mHorizontalScrollFactor);
                      if (delta != 0) {
                          final int range = getScrollRange();
                          int oldScrollX = mScrollX;
                          int newScrollX = oldScrollX + delta;
                          if (newScrollX < 0) {
                              newScrollX = 0;
                          } else if (newScrollX > range) {
                              newScrollX = range;
                          }
                          if (newScrollX != oldScrollX) {
                              super.scrollTo(newScrollX, mScrollY);
                              return true;
                          }
                      }
                  }
              }
          }
          return super.onGenericMotionEvent(event);
      }
  
      @Override
      public boolean shouldDelayChildPressedState() {
          return true;
      }
  
      @Override
      protected void onOverScrolled(int scrollX, int scrollY,
              boolean clampedX, boolean clampedY) {
          // Treat animating scrolls differently; see #computeScroll() for why.
          if (!mScroller.isFinished()) {
              final int oldX = mScrollX;
              final int oldY = mScrollY;
              mScrollX = scrollX;
              mScrollY = scrollY;
              invalidateParentIfNeeded();
              onScrollChanged(mScrollX, mScrollY, oldX, oldY);
              if (clampedX) {
                  mScroller.springBack(mScrollX, mScrollY, 0, getScrollRange(), 0, 0);
              }
          } else {
              super.scrollTo(scrollX, scrollY);
          }
  
          awakenScrollBars();
      }
  
      /** @hide */
      @Override
      public boolean performAccessibilityActionInternal(int action, Bundle arguments) {
          if (super.performAccessibilityActionInternal(action, arguments)) {
              return true;
          }
          switch (action) {
              case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD:
              case R.id.accessibilityActionScrollRight: {
                  if (!isEnabled()) {
                      return false;
                  }
                  final int viewportWidth = getWidth() - mPaddingLeft - mPaddingRight;
                  final int targetScrollX = Math.min(mScrollX + viewportWidth, getScrollRange());
                  if (targetScrollX != mScrollX) {
                      smoothScrollTo(targetScrollX, 0);
                      return true;
                  }
              } return false;
              case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD:
              case R.id.accessibilityActionScrollLeft: {
                  if (!isEnabled()) {
                      return false;
                  }
                  final int viewportWidth = getWidth() - mPaddingLeft - mPaddingRight;
                  final int targetScrollX = Math.max(0, mScrollX - viewportWidth);
                  if (targetScrollX != mScrollX) {
                      smoothScrollTo(targetScrollX, 0);
                      return true;
                  }
              } return false;
          }
          return false;
      }
  
      @Override
      public CharSequence getAccessibilityClassName() {
          return HorizontalScrollView.class.getName();
      }
  
      /** @hide */
      @Override
      public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) {
          super.onInitializeAccessibilityNodeInfoInternal(info);
          final int scrollRange = getScrollRange();
          if (scrollRange > 0) {
              info.setScrollable(true);
              if (isEnabled() && mScrollX > 0) {
                  info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_BACKWARD);
                  info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_LEFT);
              }
              if (isEnabled() && mScrollX < scrollRange) {
                  info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_FORWARD);
                  info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_RIGHT);
              }
          }
      }
  
      /** @hide */
      @Override
      public void onInitializeAccessibilityEventInternal(AccessibilityEvent event) {
          super.onInitializeAccessibilityEventInternal(event);
          event.setScrollable(getScrollRange() > 0);
          event.setScrollX(mScrollX);
          event.setScrollY(mScrollY);
          event.setMaxScrollX(getScrollRange());
          event.setMaxScrollY(mScrollY);
      }
  
      private int getScrollRange() {
          int scrollRange = 0;
          if (getChildCount() > 0) {
              View child = getChildAt(0);
              scrollRange = Math.max(0,
                      child.getWidth() - (getWidth() - mPaddingLeft - mPaddingRight));
          }
          return scrollRange;
      }
  
      /**
       * <p>
       * Finds the next focusable component that fits in this View's bounds
       * (excluding fading edges) pretending that this View's left is located at
       * the parameter left.
       * </p>
       *
       * @param leftFocus          look for a candidate is the one at the left of the bounds
       *                           if leftFocus is true, or at the right of the bounds if leftFocus
       *                           is false
       * @param left               the left offset of the bounds in which a focusable must be
       *                           found (the fading edge is assumed to start at this position)
       * @param preferredFocusable the View that has highest priority and will be
       *                           returned if it is within my bounds (null is valid)
       * @return the next focusable component in the bounds or null if none can be found
       */
      private View findFocusableViewInMyBounds(final boolean leftFocus,
              final int left, View preferredFocusable) {
          /*
           * The fading edge's transparent side should be considered for focus
           * since it's mostly visible, so we divide the actual fading edge length
           * by 2.
           */
          final int fadingEdgeLength = getHorizontalFadingEdgeLength() / 2;
          final int leftWithoutFadingEdge = left + fadingEdgeLength;
          final int rightWithoutFadingEdge = left + getWidth() - fadingEdgeLength;
  
          if ((preferredFocusable != null)
                  && (preferredFocusable.getLeft() < rightWithoutFadingEdge)
                  && (preferredFocusable.getRight() > leftWithoutFadingEdge)) {
              return preferredFocusable;
          }
  
          return findFocusableViewInBounds(leftFocus, leftWithoutFadingEdge,
                  rightWithoutFadingEdge);
      }
  
      /**
       * <p>
       * Finds the next focusable component that fits in the specified bounds.
       * </p>
       *
       * @param leftFocus look for a candidate is the one at the left of the bounds
       *                  if leftFocus is true, or at the right of the bounds if
       *                  leftFocus is false
       * @param left      the left offset of the bounds in which a focusable must be
       *                  found
       * @param right     the right offset of the bounds in which a focusable must
       *                  be found
       * @return the next focusable component in the bounds or null if none can
       *         be found
       */
      private View findFocusableViewInBounds(boolean leftFocus, int left, int right) {
  
          List<View> focusables = getFocusables(View.FOCUS_FORWARD);
          View focusCandidate = null;
  
          /*
           * A fully contained focusable is one where its left is below the bound's
           * left, and its right is above the bound's right. A partially
           * contained focusable is one where some part of it is within the
           * bounds, but it also has some part that is not within bounds.  A fully contained
           * focusable is preferred to a partially contained focusable.
           */
          boolean foundFullyContainedFocusable = false;
  
          int count = focusables.size();
          for (int i = 0; i < count; i++) {
              View view = focusables.get(i);
              int viewLeft = view.getLeft();
              int viewRight = view.getRight();
  
              if (left < viewRight && viewLeft < right) {
                  /*
                   * the focusable is in the target area, it is a candidate for
                   * focusing
                   */
  
                  final boolean viewIsFullyContained = (left < viewLeft) &&
                          (viewRight < right);
  
                  if (focusCandidate == null) {
                      /* No candidate, take this one */
                      focusCandidate = view;
                      foundFullyContainedFocusable = viewIsFullyContained;
                  } else {
                      final boolean viewIsCloserToBoundary =
                              (leftFocus && viewLeft < focusCandidate.getLeft()) ||
                                      (!leftFocus && viewRight > focusCandidate.getRight());
  
                      if (foundFullyContainedFocusable) {
                          if (viewIsFullyContained && viewIsCloserToBoundary) {
                              /*
                               * We're dealing with only fully contained views, so
                               * it has to be closer to the boundary to beat our
                               * candidate
                               */
                              focusCandidate = view;
                          }
                      } else {
                          if (viewIsFullyContained) {
                              /* Any fully contained view beats a partially contained view */
                              focusCandidate = view;
                              foundFullyContainedFocusable = true;
                          } else if (viewIsCloserToBoundary) {
                              /*
                               * Partially contained view beats another partially
                               * contained view if it's closer
                               */
                              focusCandidate = view;
                          }
                      }
                  }
              }
          }
  
          return focusCandidate;
      }
  
      /**
       * <p>Handles scrolling in response to a "page up/down" shortcut press. This
       * method will scroll the view by one page left or right and give the focus
       * to the leftmost/rightmost component in the new visible area. If no
       * component is a good candidate for focus, this scrollview reclaims the
       * focus.</p>
       *
       * @param direction the scroll direction: {@link android.view.View#FOCUS_LEFT}
       *                  to go one page left or {@link android.view.View#FOCUS_RIGHT}
       *                  to go one page right
       * @return true if the key event is consumed by this method, false otherwise
       */
      public boolean pageScroll(int direction) {
          boolean right = direction == View.FOCUS_RIGHT;
          int width = getWidth();
  
          if (right) {
              mTempRect.left = getScrollX() + width;
              int count = getChildCount();
              if (count > 0) {
                  View view = getChildAt(0);
                  if (mTempRect.left + width > view.getRight()) {
                      mTempRect.left = view.getRight() - width;
                  }
              }
          } else {
              mTempRect.left = getScrollX() - width;
              if (mTempRect.left < 0) {
                  mTempRect.left = 0;
              }
          }
          mTempRect.right = mTempRect.left + width;
  
          return scrollAndFocus(direction, mTempRect.left, mTempRect.right);
      }
  
      /**
       * <p>Handles scrolling in response to a "home/end" shortcut press. This
       * method will scroll the view to the left or right and give the focus
       * to the leftmost/rightmost component in the new visible area. If no
       * component is a good candidate for focus, this scrollview reclaims the
       * focus.</p>
       *
       * @param direction the scroll direction: {@link android.view.View#FOCUS_LEFT}
       *                  to go the left of the view or {@link android.view.View#FOCUS_RIGHT}
       *                  to go the right
       * @return true if the key event is consumed by this method, false otherwise
       */
      public boolean fullScroll(int direction) {
          boolean right = direction == View.FOCUS_RIGHT;
          int width = getWidth();
  
          mTempRect.left = 0;
          mTempRect.right = width;
  
          if (right) {
              int count = getChildCount();
              if (count > 0) {
                  View view = getChildAt(0);
                  mTempRect.right = view.getRight();
                  mTempRect.left = mTempRect.right - width;
              }
          }
  
          return scrollAndFocus(direction, mTempRect.left, mTempRect.right);
      }
  
      /**
       * <p>Scrolls the view to make the area defined by <code>left</code> and
       * <code>right</code> visible. This method attempts to give the focus
       * to a component visible in this area. If no component can be focused in
       * the new visible area, the focus is reclaimed by this scrollview.</p>
       *
       * @param direction the scroll direction: {@link android.view.View#FOCUS_LEFT}
       *                  to go left {@link android.view.View#FOCUS_RIGHT} to right
       * @param left     the left offset of the new area to be made visible
       * @param right    the right offset of the new area to be made visible
       * @return true if the key event is consumed by this method, false otherwise
       */
      private boolean scrollAndFocus(int direction, int left, int right) {
          boolean handled = true;
  
          int width = getWidth();
          int containerLeft = getScrollX();
          int containerRight = containerLeft + width;
          boolean goLeft = direction == View.FOCUS_LEFT;
  
          View newFocused = findFocusableViewInBounds(goLeft, left, right);
          if (newFocused == null) {
              newFocused = this;
          }
  
          if (left >= containerLeft && right <= containerRight) {
              handled = false;
          } else {
              int delta = goLeft ? (left - containerLeft) : (right - containerRight);
              doScrollX(delta);
          }
  
          if (newFocused != findFocus()) newFocused.requestFocus(direction);
  
          return handled;
      }
  
      /**
       * Handle scrolling in response to a left or right arrow click.
       *
       * @param direction The direction corresponding to the arrow key that was
       *                  pressed
       * @return True if we consumed the event, false otherwise
       */
      public boolean arrowScroll(int direction) {
  
          View currentFocused = findFocus();
          if (currentFocused == this) currentFocused = null;
  
          View nextFocused = FocusFinder.getInstance().findNextFocus(this, currentFocused, direction);
  
          final int maxJump = getMaxScrollAmount();
  
          if (nextFocused != null && isWithinDeltaOfScreen(nextFocused, maxJump)) {
              nextFocused.getDrawingRect(mTempRect);
              offsetDescendantRectToMyCoords(nextFocused, mTempRect);
              int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect);
              doScrollX(scrollDelta);
              nextFocused.requestFocus(direction);
          } else {
              // no new focus
              int scrollDelta = maxJump;
  
              if (direction == View.FOCUS_LEFT && getScrollX() < scrollDelta) {
                  scrollDelta = getScrollX();
              } else if (direction == View.FOCUS_RIGHT && getChildCount() > 0) {
  
                  int daRight = getChildAt(0).getRight();
  
                  int screenRight = getScrollX() + getWidth();
  
                  if (daRight - screenRight < maxJump) {
                      scrollDelta = daRight - screenRight;
                  }
              }
              if (scrollDelta == 0) {
                  return false;
              }
              doScrollX(direction == View.FOCUS_RIGHT ? scrollDelta : -scrollDelta);
          }
  
          if (currentFocused != null && currentFocused.isFocused()
                  && isOffScreen(currentFocused)) {
              // previously focused item still has focus and is off screen, give
              // it up (take it back to ourselves)
              // (also, need to temporarily force FOCUS_BEFORE_DESCENDANTS so we are
              // sure to
              // get it)
              final int descendantFocusability = getDescendantFocusability();  // save
              setDescendantFocusability(ViewGroup.FOCUS_BEFORE_DESCENDANTS);
              requestFocus();
              setDescendantFocusability(descendantFocusability);  // restore
          }
          return true;
      }
  
      /**
       * @return whether the descendant of this scroll view is scrolled off
       *  screen.
       */
      private boolean isOffScreen(View descendant) {
          return !isWithinDeltaOfScreen(descendant, 0);
      }
  
      /**
       * @return whether the descendant of this scroll view is within delta
       *  pixels of being on the screen.
       */
      private boolean isWithinDeltaOfScreen(View descendant, int delta) {
          descendant.getDrawingRect(mTempRect);
          offsetDescendantRectToMyCoords(descendant, mTempRect);
  
          return (mTempRect.right + delta) >= getScrollX()
                  && (mTempRect.left - delta) <= (getScrollX() + getWidth());
      }
  
      /**
       * Smooth scroll by a X delta
       *
       * @param delta the number of pixels to scroll by on the X axis
       */
      private void doScrollX(int delta) {
          if (delta != 0) {
              if (mSmoothScrollingEnabled) {
                  smoothScrollBy(delta, 0);
              } else {
                  scrollBy(delta, 0);
              }
          }
      }
  
      /**
       * Like {@link View#scrollBy}, but scroll smoothly instead of immediately.
       *
       * @param dx the number of pixels to scroll by on the X axis
       * @param dy the number of pixels to scroll by on the Y axis
       */
      public final void smoothScrollBy(int dx, int dy) {
          if (getChildCount() == 0) {
              // Nothing to do.
              return;
          }
          long duration = AnimationUtils.currentAnimationTimeMillis() - mLastScroll;
          if (duration > ANIMATED_SCROLL_GAP) {
              final int width = getWidth() - mPaddingRight - mPaddingLeft;
              final int right = getChildAt(0).getWidth();
              final int maxX = Math.max(0, right - width);
              final int scrollX = mScrollX;
              dx = Math.max(0, Math.min(scrollX + dx, maxX)) - scrollX;
  
              mScroller.startScroll(scrollX, mScrollY, dx, 0);
              postInvalidateOnAnimation();
          } else {
              if (!mScroller.isFinished()) {
                  mScroller.abortAnimation();
              }
              scrollBy(dx, dy);
          }
          mLastScroll = AnimationUtils.currentAnimationTimeMillis();
      }
  
      /**
       * Like {@link #scrollTo}, but scroll smoothly instead of immediately.
       *
       * @param x the position where to scroll on the X axis
       * @param y the position where to scroll on the Y axis
       */
      public final void smoothScrollTo(int x, int y) {
          smoothScrollBy(x - mScrollX, y - mScrollY);
      }
  
      /**
       * <p>The scroll range of a scroll view is the overall width of all of its
       * children.</p>
       */
      @Override
      protected int computeHorizontalScrollRange() {
          final int count = getChildCount();
          final int contentWidth = getWidth() - mPaddingLeft - mPaddingRight;
          if (count == 0) {
              return contentWidth;
          }
  
          int scrollRange = getChildAt(0).getRight();
          final int scrollX = mScrollX;
          final int overscrollRight = Math.max(0, scrollRange - contentWidth);
          if (scrollX < 0) {
              scrollRange -= scrollX;
          } else if (scrollX > overscrollRight) {
              scrollRange += scrollX - overscrollRight;
          }
  
          return scrollRange;
      }
  
      @Override
      protected int computeHorizontalScrollOffset() {
          return Math.max(0, super.computeHorizontalScrollOffset());
      }
  
      @Override
      protected void measureChild(View child, int parentWidthMeasureSpec,
              int parentHeightMeasureSpec) {
          ViewGroup.LayoutParams lp = child.getLayoutParams();
  
          final int horizontalPadding = mPaddingLeft + mPaddingRight;
          final int childWidthMeasureSpec = MeasureSpec.makeSafeMeasureSpec(
                  Math.max(0, MeasureSpec.getSize(parentWidthMeasureSpec) - horizontalPadding),
                  MeasureSpec.UNSPECIFIED);
  
          final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                  mPaddingTop + mPaddingBottom, lp.height);
          child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
      }
  
      @Override
      protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed,
              int parentHeightMeasureSpec, int heightUsed) {
          final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
  
          final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                  mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
                          + heightUsed, lp.height);
          final int usedTotal = mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin +
                  widthUsed;
          final int childWidthMeasureSpec = MeasureSpec.makeSafeMeasureSpec(
                  Math.max(0, MeasureSpec.getSize(parentWidthMeasureSpec) - usedTotal),
                  MeasureSpec.UNSPECIFIED);
  
          child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
      }
  
      @Override
      public void computeScroll() {
          if (mScroller.computeScrollOffset()) {
              // This is called at drawing time by ViewGroup.  We don't want to
              // re-show the scrollbars at this point, which scrollTo will do,
              // so we replicate most of scrollTo here.
              //
              //         It's a little odd to call onScrollChanged from inside the drawing.
              //
              //         It is, except when you remember that computeScroll() is used to
              //         animate scrolling. So unless we want to defer the onScrollChanged()
              //         until the end of the animated scrolling, we don't really have a
              //         choice here.
              //
              //         I agree.  The alternative, which I think would be worse, is to post
              //         something and tell the subclasses later.  This is bad because there
              //         will be a window where mScrollX/Y is different from what the app
              //         thinks it is.
              //
              int oldX = mScrollX;
              int oldY = mScrollY;
              int x = mScroller.getCurrX();
              int y = mScroller.getCurrY();
  
              if (oldX != x || oldY != y) {
                  final int range = getScrollRange();
                  final int overscrollMode = getOverScrollMode();
                  final boolean canOverscroll = overscrollMode == OVER_SCROLL_ALWAYS ||
                          (overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0);
  
                  overScrollBy(x - oldX, y - oldY, oldX, oldY, range, 0,
                          mOverflingDistance, 0, false);
                  onScrollChanged(mScrollX, mScrollY, oldX, oldY);
  
                  if (canOverscroll) {
                      if (x < 0 && oldX >= 0) {
                          mEdgeGlowLeft.onAbsorb((int) mScroller.getCurrVelocity());
                      } else if (x > range && oldX <= range) {
                          mEdgeGlowRight.onAbsorb((int) mScroller.getCurrVelocity());
                      }
                  }
              }
  
              if (!awakenScrollBars()) {
                  postInvalidateOnAnimation();
              }
          }
      }
  
      /**
       * Scrolls the view to the given child.
       *
       * @param child the View to scroll to
       */
      private void scrollToChild(View child) {
          child.getDrawingRect(mTempRect);
  
          /* Offset from child's local coordinates to ScrollView coordinates */
          offsetDescendantRectToMyCoords(child, mTempRect);
  
          int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect);
  
          if (scrollDelta != 0) {
              scrollBy(scrollDelta, 0);
          }
      }
  
      /**
       * If rect is off screen, scroll just enough to get it (or at least the
       * first screen size chunk of it) on screen.
       *
       * @param rect      The rectangle.
       * @param immediate True to scroll immediately without animation
       * @return true if scrolling was performed
       */
      private boolean scrollToChildRect(Rect rect, boolean immediate) {
          final int delta = computeScrollDeltaToGetChildRectOnScreen(rect);
          final boolean scroll = delta != 0;
          if (scroll) {
              if (immediate) {
                  scrollBy(delta, 0);
              } else {
                  smoothScrollBy(delta, 0);
              }
          }
          return scroll;
      }
  
      /**
       * Compute the amount to scroll in the X direction in order to get
       * a rectangle completely on the screen (or, if taller than the screen,
       * at least the first screen size chunk of it).
       *
       * @param rect The rect.
       * @return The scroll delta.
       */
      protected int computeScrollDeltaToGetChildRectOnScreen(Rect rect) {
          if (getChildCount() == 0) return 0;
  
          int width = getWidth();
          int screenLeft = getScrollX();
          int screenRight = screenLeft + width;
  
          int fadingEdge = getHorizontalFadingEdgeLength();
  
          // leave room for left fading edge as long as rect isn't at very left
          if (rect.left > 0) {
              screenLeft += fadingEdge;
          }
  
          // leave room for right fading edge as long as rect isn't at very right
          if (rect.right < getChildAt(0).getWidth()) {
              screenRight -= fadingEdge;
          }
  
          int scrollXDelta = 0;
  
          if (rect.right > screenRight && rect.left > screenLeft) {
              // need to move right to get it in view: move right just enough so
              // that the entire rectangle is in view (or at least the first
              // screen size chunk).
  
              if (rect.width() > width) {
                  // just enough to get screen size chunk on
                  scrollXDelta += (rect.left - screenLeft);
              } else {
                  // get entire rect at right of screen
                  scrollXDelta += (rect.right - screenRight);
              }
  
              // make sure we aren't scrolling beyond the end of our content
              int right = getChildAt(0).getRight();
              int distanceToRight = right - screenRight;
              scrollXDelta = Math.min(scrollXDelta, distanceToRight);
  
          } else if (rect.left < screenLeft && rect.right < screenRight) {
              // need to move right to get it in view: move right just enough so that
              // entire rectangle is in view (or at least the first screen
              // size chunk of it).
  
              if (rect.width() > width) {
                  // screen size chunk
                  scrollXDelta -= (screenRight - rect.right);
              } else {
                  // entire rect at left
                  scrollXDelta -= (screenLeft - rect.left);
              }
  
              // make sure we aren't scrolling any further than the left our content
              scrollXDelta = Math.max(scrollXDelta, -getScrollX());
          }
          return scrollXDelta;
      }
  
      @Override
      public void requestChildFocus(View child, View focused) {
          if (focused != null && focused.getRevealOnFocusHint()) {
              if (!mIsLayoutDirty) {
                  scrollToChild(focused);
              } else {
                  // The child may not be laid out yet, we can't compute the scroll yet
                  mChildToScrollTo = focused;
              }
          }
          super.requestChildFocus(child, focused);
      }
  
  
      /**
       * When looking for focus in children of a scroll view, need to be a little
       * more careful not to give focus to something that is scrolled off screen.
       *
       * This is more expensive than the default {@link android.view.ViewGroup}
       * implementation, otherwise this behavior might have been made the default.
       */
      @Override
      protected boolean onRequestFocusInDescendants(int direction,
              Rect previouslyFocusedRect) {
  
          // convert from forward / backward notation to up / down / left / right
          // (ugh).
          if (direction == View.FOCUS_FORWARD) {
              direction = View.FOCUS_RIGHT;
          } else if (direction == View.FOCUS_BACKWARD) {
              direction = View.FOCUS_LEFT;
          }
  
          final View nextFocus = previouslyFocusedRect == null ?
                  FocusFinder.getInstance().findNextFocus(this, null, direction) :
                  FocusFinder.getInstance().findNextFocusFromRect(this,
                          previouslyFocusedRect, direction);
  
          if (nextFocus == null) {
              return false;
          }
  
          if (isOffScreen(nextFocus)) {
              return false;
          }
  
          return nextFocus.requestFocus(direction, previouslyFocusedRect);
      }
  
      @Override
      public boolean requestChildRectangleOnScreen(View child, Rect rectangle,
              boolean immediate) {
          // offset into coordinate space of this scroll view
          rectangle.offset(child.getLeft() - child.getScrollX(),
                  child.getTop() - child.getScrollY());
  
          return scrollToChildRect(rectangle, immediate);
      }
  
      @Override
      public void requestLayout() {
          mIsLayoutDirty = true;
          super.requestLayout();
      }
  
      @Override
      protected void onLayout(boolean changed, int l, int t, int r, int b) {
          int childWidth = 0;
          int childMargins = 0;
  
          if (getChildCount() > 0) {
              childWidth = getChildAt(0).getMeasuredWidth();
              LayoutParams childParams = (LayoutParams) getChildAt(0).getLayoutParams();
              childMargins = childParams.leftMargin + childParams.rightMargin;
          }
  
          final int available = r - l - getPaddingLeftWithForeground() -
                  getPaddingRightWithForeground() - childMargins;
  
          final boolean forceLeftGravity = (childWidth > available);
  
          layoutChildren(l, t, r, b, forceLeftGravity);
  
          mIsLayoutDirty = false;
          // Give a child focus if it needs it
          if (mChildToScrollTo != null && isViewDescendantOf(mChildToScrollTo, this)) {
              scrollToChild(mChildToScrollTo);
          }
          mChildToScrollTo = null;
  
          if (!isLaidOut()) {
              final int scrollRange = Math.max(0,
                      childWidth - (r - l - mPaddingLeft - mPaddingRight));
              if (mSavedState != null) {
                  mScrollX = isLayoutRtl()
                          ? scrollRange - mSavedState.scrollOffsetFromStart
                          : mSavedState.scrollOffsetFromStart;
                  mSavedState = null;
              } else {
                  if (isLayoutRtl()) {
                      mScrollX = scrollRange - mScrollX;
                  } // mScrollX default value is "0" for LTR
              }
              // Don't forget to clamp
              if (mScrollX > scrollRange) {
                  mScrollX = scrollRange;
              } else if (mScrollX < 0) {
                  mScrollX = 0;
              }
          }
  
          // Calling this with the present values causes it to re-claim them
          scrollTo(mScrollX, mScrollY);
      }
  
      @Override
      protected void onSizeChanged(int w, int h, int oldw, int oldh) {
          super.onSizeChanged(w, h, oldw, oldh);
  
          View currentFocused = findFocus();
          if (null == currentFocused || this == currentFocused)
              return;
  
          final int maxJump = mRight - mLeft;
  
          if (isWithinDeltaOfScreen(currentFocused, maxJump)) {
              currentFocused.getDrawingRect(mTempRect);
              offsetDescendantRectToMyCoords(currentFocused, mTempRect);
              int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect);
              doScrollX(scrollDelta);
          }
      }
  
      /**
       * Return true if child is a descendant of parent, (or equal to the parent).
       */
      private static boolean isViewDescendantOf(View child, View parent) {
          if (child == parent) {
              return true;
          }
  
          final ViewParent theParent = child.getParent();
          return (theParent instanceof ViewGroup) && isViewDescendantOf((View) theParent, parent);
      }
  
      /**
       * Fling the scroll view
       *
       * @param velocityX The initial velocity in the X direction. Positive
       *                  numbers mean that the finger/cursor is moving down the screen,
       *                  which means we want to scroll towards the left.
       */
      public void fling(int velocityX) {
          if (getChildCount() > 0) {
              int width = getWidth() - mPaddingRight - mPaddingLeft;
              int right = getChildAt(0).getWidth();
  
              mScroller.fling(mScrollX, mScrollY, velocityX, 0, 0,
                      Math.max(0, right - width), 0, 0, width/2, 0);
  
              final boolean movingRight = velocityX > 0;
  
              View currentFocused = findFocus();
              View newFocused = findFocusableViewInMyBounds(movingRight,
                      mScroller.getFinalX(), currentFocused);
  
              if (newFocused == null) {
                  newFocused = this;
              }
  
              if (newFocused != currentFocused) {
                  newFocused.requestFocus(movingRight ? View.FOCUS_RIGHT : View.FOCUS_LEFT);
              }
  
              postInvalidateOnAnimation();
          }
      }
  
      /**
       * {@inheritDoc}
       *
       * <p>This version also clamps the scrolling to the bounds of our child.
       */
      @Override
      public void scrollTo(int x, int y) {
          // we rely on the fact the View.scrollBy calls scrollTo.
          if (getChildCount() > 0) {
              View child = getChildAt(0);
              x = clamp(x, getWidth() - mPaddingRight - mPaddingLeft, child.getWidth());
              y = clamp(y, getHeight() - mPaddingBottom - mPaddingTop, child.getHeight());
              if (x != mScrollX || y != mScrollY) {
                  super.scrollTo(x, y);
              }
          }
      }
  
      private boolean shouldDisplayEdgeEffects() {
          return getOverScrollMode() != OVER_SCROLL_NEVER;
      }
  
      @SuppressWarnings({"SuspiciousNameCombination"})
      @Override
      public void draw(Canvas canvas) {
          super.draw(canvas);
          if (shouldDisplayEdgeEffects()) {
              final int scrollX = mScrollX;
              if (!mEdgeGlowLeft.isFinished()) {
                  final int restoreCount = canvas.save();
                  final int height = getHeight() - mPaddingTop - mPaddingBottom;
  
                  canvas.rotate(270);
                  canvas.translate(-height + mPaddingTop, Math.min(0, scrollX));
                  mEdgeGlowLeft.setSize(height, getWidth());
                  if (mEdgeGlowLeft.draw(canvas)) {
                      postInvalidateOnAnimation();
                  }
                  canvas.restoreToCount(restoreCount);
              }
              if (!mEdgeGlowRight.isFinished()) {
                  final int restoreCount = canvas.save();
                  final int width = getWidth();
                  final int height = getHeight() - mPaddingTop - mPaddingBottom;
  
                  canvas.rotate(90);
                  canvas.translate(-mPaddingTop,
                          -(Math.max(getScrollRange(), scrollX) + width));
                  mEdgeGlowRight.setSize(height, width);
                  if (mEdgeGlowRight.draw(canvas)) {
                      postInvalidateOnAnimation();
                  }
                  canvas.restoreToCount(restoreCount);
              }
          }
      }
  
      private static int clamp(int n, int my, int child) {
          if (my >= child || n < 0) {
              return 0;
          }
          if ((my + n) > child) {
              return child - my;
          }
          return n;
      }
  
      @Override
      protected void onRestoreInstanceState(Parcelable state) {
          if (mContext.getApplicationInfo().targetSdkVersion <= Build.VERSION_CODES.JELLY_BEAN_MR2) {
              // Some old apps reused IDs in ways they shouldn't have.
              // Don't break them, but they don't get scroll state restoration.
              super.onRestoreInstanceState(state);
              return;
          }
          SavedState ss = (SavedState) state;
          super.onRestoreInstanceState(ss.getSuperState());
          mSavedState = ss;
          requestLayout();
      }
  
      @Override
      protected Parcelable onSaveInstanceState() {
          if (mContext.getApplicationInfo().targetSdkVersion <= Build.VERSION_CODES.JELLY_BEAN_MR2) {
              // Some old apps reused IDs in ways they shouldn't have.
              // Don't break them, but they don't get scroll state restoration.
              return super.onSaveInstanceState();
          }
          Parcelable superState = super.onSaveInstanceState();
          SavedState ss = new SavedState(superState);
          ss.scrollOffsetFromStart = isLayoutRtl() ? -mScrollX : mScrollX;
          return ss;
      }
  
      /** @hide */
      @Override
      protected void encodeProperties(@NonNull ViewHierarchyEncoder encoder) {
          super.encodeProperties(encoder);
          encoder.addProperty("layout:fillViewPort", mFillViewport);
      }
  
      static class SavedState extends BaseSavedState {
          public int scrollOffsetFromStart;
  
          SavedState(Parcelable superState) {
              super(superState);
          }
  
          public SavedState(Parcel source) {
              super(source);
              scrollOffsetFromStart = source.readInt();
          }
  
          @Override
          public void writeToParcel(Parcel dest, int flags) {
              super.writeToParcel(dest, flags);
              dest.writeInt(scrollOffsetFromStart);
          }
  
          @Override
          public String toString() {
              return "HorizontalScrollView.SavedState{"
                      + Integer.toHexString(System.identityHashCode(this))
                      + " scrollPosition=" + scrollOffsetFromStart
                      + "}";
          }
  
          public static final @android.annotation.NonNull Parcelable.Creator<SavedState> CREATOR
                  = new Parcelable.Creator<SavedState>() {
              public SavedState createFromParcel(Parcel in) {
                  return new SavedState(in);
              }
  
              public SavedState[] newArray(int size) {
                  return new SavedState[size];
              }
          };
      }
  }
  