private void initView(Context context) {
    mPaint = new Paint();
    mPaint.setColor(Color.WHITE);
    Resources resources = context.getResources();

    // get viewConfiguration
    mClickTimeout = ViewConfiguration.getPressedStateDuration() + ViewConfiguration.getTapTimeout();
    mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();

    // get Bitmap
    mBottom = BitmapFactory.decodeResource(resources, R.drawable.bottom);
    mBtnPressed = BitmapFactory.decodeResource(resources, R.drawable.btn_pressed);
    mBtnNormal = BitmapFactory.decodeResource(resources, R.drawable.btn_unpressed);
    mFrame = BitmapFactory.decodeResource(resources, R.drawable.frame);
    mMask = BitmapFactory.decodeResource(resources, R.drawable.mask);
    mCurBtnPic = mBtnNormal;

    mBtnWidth = mBtnPressed.getWidth();
    mMaskWidth = mMask.getWidth();
    mMaskHeight = mMask.getHeight();

    mBtnOffPos = mBtnWidth / 2;
    mBtnOnPos = mMaskWidth - mBtnWidth / 2;

    mBtnPos = mChecked ? mBtnOnPos : mBtnOffPos;
    mRealPos = getRealPos(mBtnPos);

    final float density = getResources().getDisplayMetrics().density;
    mVelocity = (int) (VELOCITY * density + 0.5f);
    mExtendOffsetY = (int) (EXTENDED_OFFSET_Y * density + 0.5f);

    mSaveLayerRectF =
        new RectF(0, mExtendOffsetY, mMask.getWidth(), mMask.getHeight() + mExtendOffsetY);
    mXfermode = new PorterDuffXfermode(PorterDuff.Mode.SRC_IN);
  }
Beispiel #2
0
  private void init(Context context, AttributeSet attrs) {
    mPaint = new Paint();
    mPaint.setColor(Color.WHITE);
    Resources resources = context.getResources();

    // get attrConfiguration
    TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.SwitchButton);
    int width = (int) array.getDimensionPixelSize(R.styleable.SwitchButton_bmWidth, 0);
    int height = (int) array.getDimensionPixelSize(R.styleable.SwitchButton_bmHeight, 0);
    array.recycle();

    // size width or height
    if (width <= 0 || height <= 0) {
      width = COMMON_WIDTH_IN_PIXEL;
      height = COMMON_HEIGHT_IN_PIXEL;
    } else {
      float scale = (float) COMMON_WIDTH_IN_PIXEL / COMMON_HEIGHT_IN_PIXEL;
      if ((float) width / height > scale) {
        width = (int) (height * scale);
      } else if ((float) width / height < scale) {
        height = (int) (width / scale);
      }
    }

    // get viewConfiguration
    mClickTimeout = ViewConfiguration.getPressedStateDuration() + ViewConfiguration.getTapTimeout();
    mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();

    // get Bitmap
    bmBgGreen = BitmapFactory.decodeResource(resources, R.drawable.switch_btn_bg_green);
    bmBgWhite = BitmapFactory.decodeResource(resources, R.drawable.switch_btn_bg_white);
    bmBtnNormal = BitmapFactory.decodeResource(resources, R.drawable.switch_btn_normal);
    bmBtnPressed = BitmapFactory.decodeResource(resources, R.drawable.switch_btn_pressed);

    // size Bitmap
    bmBgGreen = Bitmap.createScaledBitmap(bmBgGreen, width, height, true);
    bmBgWhite = Bitmap.createScaledBitmap(bmBgWhite, width, height, true);
    bmBtnNormal = Bitmap.createScaledBitmap(bmBtnNormal, height, height, true);
    bmBtnPressed = Bitmap.createScaledBitmap(bmBtnPressed, height, height, true);

    bmCurBtnPic = bmBtnNormal; // 初始按钮图片
    bmCurBgPic = mChecked ? bmBgGreen : bmBgWhite; // 初始背景图片
    bgWidth = bmBgGreen.getWidth(); // 背景宽度
    bgHeight = bmBgGreen.getHeight(); // 背景高度
    btnWidth = bmBtnNormal.getWidth(); // 按钮宽度
    offBtnPos = 0; // 关闭时在最左边
    onBtnPos = bgWidth - btnWidth; // 开始时在右边
    curBtnPos = mChecked ? onBtnPos : offBtnPos; // 按钮当前为初始位置

    // get density
    float density = resources.getDisplayMetrics().density;
    mVelocity = (int) (VELOCITY * density + 0.5f); // 动画距离
    mSaveLayerRectF = new RectF(0, 0, bgWidth, bgHeight);
  }
  public RadialPickerLayout(Context context, AttributeSet attrs) {
    super(context, attrs);

    setOnTouchListener(this);
    ViewConfiguration vc = ViewConfiguration.get(context);
    TOUCH_SLOP = vc.getScaledTouchSlop();
    TAP_TIMEOUT = ViewConfiguration.getTapTimeout();
    mDoingMove = false;

    mCircleView = new CircleView(context);
    addView(mCircleView);

    mAmPmCirclesView = new AmPmCirclesView(context);
    addView(mAmPmCirclesView);

    mHourRadialTextsView = new RadialTextsView(context);
    addView(mHourRadialTextsView);
    mMinuteRadialTextsView = new RadialTextsView(context);
    addView(mMinuteRadialTextsView);

    mHourRadialSelectorView = new RadialSelectorView(context);
    addView(mHourRadialSelectorView);
    mMinuteRadialSelectorView = new RadialSelectorView(context);
    addView(mMinuteRadialSelectorView);

    // Prepare mapping to snap touchable degrees to selectable degrees.
    preparePrefer30sMap();

    mVibrator = (Vibrator) context.getSystemService(Service.VIBRATOR_SERVICE);
    mLastVibrate = 0;
    mLastValueSelected = -1;

    mInputEnabled = true;
    mGrayBox = new View(context);
    mGrayBox.setLayoutParams(
        new ViewGroup.LayoutParams(
            ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
    mGrayBox.setBackgroundColor(getResources().getColor(R.color.transparent_black));
    mGrayBox.setVisibility(View.INVISIBLE);
    addView(mGrayBox);

    mAccessibilityManager =
        (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE);

    mTimeInitialized = false;
  }
Beispiel #4
0
  /**
   * Setup the thread.
   *
   * @param activity The activity
   * @param renderSetting
   * @param surfaceConfig EGL surface configuration
   * @param pixelFormat PIXELFORMAT_RGBA_4444 PIXELFORMAT_RGBA_5551 PIXELFORMAT_RGBA_8888
   *     PIXELFORMAT_RGBX_8888 PIXELFORMAT_RGB_565 PIXELFORMAT_RGB_888
   * @throws IllegalArgumentException if renderer is not 'GLES2', or pixelFormat not one of the
   *     listed values or runner is null.
   */
  private void setup(
      Activity activity,
      RenderSetting renderSetting,
      SurfaceConfiguration surfaceConfig,
      int pixelFormat,
      String renderer,
      CompatibilityRunner runner) {
    Log.d(TAG, "Creating new instance of OpenGLENThread with runner: " + runner);
    if (runner == null) {
      throw new IllegalArgumentException("Runner class is null");
    }
    if (renderer == null) {
      throw new IllegalArgumentException("Renderer is null");
    }
    if (!renderer.equalsIgnoreCase("GLES2")) {
      throw new IllegalArgumentException("Invalid renderer:" + renderer);
    }
    if (pixelFormat != ConstantValues.PIXELFORMAT_RGB_565
        && pixelFormat != ConstantValues.PIXELFORMAT_RGB_888
        && pixelFormat != ConstantValues.PIXELFORMAT_RGBA_4444
        && pixelFormat != ConstantValues.PIXELFORMAT_RGBA_5551
        && pixelFormat != ConstantValues.PIXELFORMAT_RGBA_8888
        && pixelFormat != ConstantValues.PIXELFORMAT_RGBX_8888) {
      throw new IllegalArgumentException(INVALID_PIXELFORMAT + pixelFormat);
    }

    mTouchSlop = ViewConfiguration.getTouchSlop();
    mLongPressThreshold = ViewConfiguration.getLongPressTimeout();
    mTapTimeout = ViewConfiguration.getTapTimeout();
    mSurfaceConfig = surfaceConfig;
    mRenderSetting = renderSetting;
    mSurfaceHolder = getHolder();
    mSurfaceHolder.addCallback(this);
    mActivity = activity;
    mRenderStr = renderer;
    if (mRunner != null) {
      throw new IllegalArgumentException("Runner is not null, release before setting new.");
    }
    mRunner = runner;
    mAM =
        (ActivityManager)
            mActivity.getApplicationContext().getSystemService(Context.ACTIVITY_SERVICE);
  }
  private void initView(Context context) {
    mPaint = new Paint();
    mPaint.setColor(Color.WHITE);
    Resources resources = context.getResources();

    // get viewConfiguration
    mClickTimeout = ViewConfiguration.getPressedStateDuration() + ViewConfiguration.getTapTimeout();
    mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();

    // get Bitmap
    mBottom = BitmapFactory.decodeResource(resources, R.drawable.kaiguan);
    mBtnPressed = BitmapFactory.decodeResource(resources, R.drawable.yuan);
    mBtnNormal = BitmapFactory.decodeResource(resources, R.drawable.yuan);
    mFrame = BitmapFactory.decodeResource(resources, R.drawable.guan);
    mMask = BitmapFactory.decodeResource(resources, R.drawable.kai);
    mCurBtnPic = mBtnNormal;

    mBtnWidth = mBtnPressed.getWidth();
    mMaskWidth = mMask.getWidth();
    mMaskHeight = mMask.getHeight();

    mBtnOffPos = mBtnWidth / 2;
    mBtnOnPos = mMaskWidth - mBtnWidth / 2;
    // 判断起始位置,如果设定了mChecked为true,起始位置为 mBtnOnPos
    mBtnPos = mChecked ? mBtnOnPos : mBtnOffPos;
    mRealPos = getRealPos(mBtnPos);
    // density 密度
    final float density = getResources().getDisplayMetrics().density; // 方法是获取资源密度(Density)
    mVelocity = (int) (VELOCITY * density + 0.5f);
    mExtendOffsetY = (int) (EXTENDED_OFFSET_Y * density + 0.5f);
    // 创建一个新的矩形与指定的坐标。
    mSaveLayerRectF =
        new RectF(0, mExtendOffsetY, mMask.getWidth(), mMask.getHeight() + mExtendOffsetY);
    mXfermode = new PorterDuffXfermode(PorterDuff.Mode.SRC_IN); // PorterDuff.Mode.SRC_IN
    // :这个属性代表
    // 取两层绘制交集。显示上层。
  }
  @Test
  public void methodsShouldReturnAndroidConstants() {
    Activity context = new Activity();
    ViewConfiguration viewConfiguration = ViewConfiguration.get(context);

    assertEquals(10, ViewConfiguration.getScrollBarSize());
    assertEquals(250, ViewConfiguration.getScrollBarFadeDuration());
    assertEquals(300, ViewConfiguration.getScrollDefaultDelay());
    assertEquals(12, ViewConfiguration.getFadingEdgeLength());
    assertEquals(125, ViewConfiguration.getPressedStateDuration());
    assertEquals(500, ViewConfiguration.getLongPressTimeout());
    assertEquals(115, ViewConfiguration.getTapTimeout());
    assertEquals(500, ViewConfiguration.getJumpTapTimeout());
    assertEquals(300, ViewConfiguration.getDoubleTapTimeout());
    assertEquals(12, ViewConfiguration.getEdgeSlop());
    assertEquals(16, ViewConfiguration.getTouchSlop());
    assertEquals(16, ViewConfiguration.getWindowTouchSlop());
    assertEquals(50, ViewConfiguration.getMinimumFlingVelocity());
    assertEquals(4000, ViewConfiguration.getMaximumFlingVelocity());
    assertEquals(320 * 480 * 4, ViewConfiguration.getMaximumDrawingCacheSize());
    assertEquals(3000, ViewConfiguration.getZoomControlsTimeout());
    assertEquals(500, ViewConfiguration.getGlobalActionKeyTimeout());
    assertEquals(0.015f, ViewConfiguration.getScrollFriction());

    assertEquals(1f, context.getResources().getDisplayMetrics().density);

    assertEquals(10, viewConfiguration.getScaledScrollBarSize());
    assertEquals(12, viewConfiguration.getScaledFadingEdgeLength());
    assertEquals(12, viewConfiguration.getScaledEdgeSlop());
    assertEquals(16, viewConfiguration.getScaledTouchSlop());
    assertEquals(32, viewConfiguration.getScaledPagingTouchSlop());
    assertEquals(100, viewConfiguration.getScaledDoubleTapSlop());
    assertEquals(16, viewConfiguration.getScaledWindowTouchSlop());
    assertEquals(50, viewConfiguration.getScaledMinimumFlingVelocity());
    assertEquals(4000, viewConfiguration.getScaledMaximumFlingVelocity());
  }
  static class GestureDetectorCompatImplBase implements GestureDetectorCompatImpl {
    private int mTouchSlopSquare;
    private int mDoubleTapSlopSquare;
    private int mMinimumFlingVelocity;
    private int mMaximumFlingVelocity;

    private static final int LONGPRESS_TIMEOUT = ViewConfiguration.getLongPressTimeout();
    private static final int TAP_TIMEOUT = ViewConfiguration.getTapTimeout();
    private static final int DOUBLE_TAP_TIMEOUT = ViewConfiguration.getDoubleTapTimeout();

    // constants for Message.what used by GestureHandler below
    private static final int SHOW_PRESS = 1;
    private static final int LONG_PRESS = 2;
    private static final int TAP = 3;

    private final Handler mHandler;
    private final OnGestureListener mListener;
    private OnDoubleTapListener mDoubleTapListener;

    private boolean mStillDown;
    private boolean mInLongPress;
    private boolean mAlwaysInTapRegion;
    private boolean mAlwaysInBiggerTapRegion;

    private MotionEvent mCurrentDownEvent;
    private MotionEvent mPreviousUpEvent;

    /**
     * True when the user is still touching for the second tap (down, move, and up events). Can only
     * be true if there is a double tap listener attached.
     */
    private boolean mIsDoubleTapping;

    private float mLastFocusX;
    private float mLastFocusY;
    private float mDownFocusX;
    private float mDownFocusY;

    private boolean mIsLongpressEnabled;

    /** Determines speed during touch scrolling */
    private VelocityTracker mVelocityTracker;

    private class GestureHandler extends Handler {
      GestureHandler() {
        super();
      }

      GestureHandler(Handler handler) {
        super(handler.getLooper());
      }

      @Override
      public void handleMessage(Message msg) {
        switch (msg.what) {
          case SHOW_PRESS:
            mListener.onShowPress(mCurrentDownEvent);
            break;

          case LONG_PRESS:
            dispatchLongPress();
            break;

          case TAP:
            // If the user's finger is still down, do not count it as a tap
            if (mDoubleTapListener != null && !mStillDown) {
              mDoubleTapListener.onSingleTapConfirmed(mCurrentDownEvent);
            }
            break;

          default:
            throw new RuntimeException("Unknown message " + msg); // never
        }
      }
    }

    /**
     * Creates a GestureDetector with the supplied listener. You may only use this constructor from
     * a UI thread (this is the usual situation).
     *
     * @see android.os.Handler#Handler()
     * @param context the application's context
     * @param listener the listener invoked for all the callbacks, this must not be null.
     * @param handler the handler to use
     * @throws NullPointerException if {@code listener} is null.
     */
    public GestureDetectorCompatImplBase(
        Context context, OnGestureListener listener, Handler handler) {
      if (handler != null) {
        mHandler = new GestureHandler(handler);
      } else {
        mHandler = new GestureHandler();
      }
      mListener = listener;
      if (listener instanceof OnDoubleTapListener) {
        setOnDoubleTapListener((OnDoubleTapListener) listener);
      }
      init(context);
    }

    private void init(Context context) {
      if (context == null) {
        throw new IllegalArgumentException("Context must not be null");
      }
      if (mListener == null) {
        throw new IllegalArgumentException("OnGestureListener must not be null");
      }
      mIsLongpressEnabled = true;

      final ViewConfiguration configuration = ViewConfiguration.get(context);
      final int touchSlop = configuration.getScaledTouchSlop();
      final int doubleTapSlop = configuration.getScaledDoubleTapSlop();
      mMinimumFlingVelocity = configuration.getScaledMinimumFlingVelocity();
      mMaximumFlingVelocity = configuration.getScaledMaximumFlingVelocity();

      mTouchSlopSquare = touchSlop * touchSlop;
      mDoubleTapSlopSquare = doubleTapSlop * doubleTapSlop;
    }

    /**
     * Sets the listener which will be called for double-tap and related gestures.
     *
     * @param onDoubleTapListener the listener invoked for all the callbacks, or null to stop
     *     listening for double-tap gestures.
     */
    public void setOnDoubleTapListener(OnDoubleTapListener onDoubleTapListener) {
      mDoubleTapListener = onDoubleTapListener;
    }

    /**
     * Set whether longpress is enabled, if this is enabled when a user presses and holds down you
     * get a longpress event and nothing further. If it's disabled the user can press and hold down
     * and then later moved their finger and you will get scroll events. By default longpress is
     * enabled.
     *
     * @param isLongpressEnabled whether longpress should be enabled.
     */
    public void setIsLongpressEnabled(boolean isLongpressEnabled) {
      mIsLongpressEnabled = isLongpressEnabled;
    }

    /** @return true if longpress is enabled, else false. */
    public boolean isLongpressEnabled() {
      return mIsLongpressEnabled;
    }

    /**
     * Analyzes the given motion event and if applicable triggers the appropriate callbacks on the
     * {@link OnGestureListener} supplied.
     *
     * @param ev The current motion event.
     * @return true if the {@link OnGestureListener} consumed the event, else false.
     */
    public boolean onTouchEvent(MotionEvent ev) {
      final int action = ev.getAction();

      if (mVelocityTracker == null) {
        mVelocityTracker = VelocityTracker.obtain();
      }
      mVelocityTracker.addMovement(ev);

      final boolean pointerUp =
          (action & MotionEventCompat.ACTION_MASK) == MotionEventCompat.ACTION_POINTER_UP;
      final int skipIndex = pointerUp ? MotionEventCompat.getActionIndex(ev) : -1;

      // Determine focal point
      float sumX = 0, sumY = 0;
      final int count = MotionEventCompat.getPointerCount(ev);
      for (int i = 0; i < count; i++) {
        if (skipIndex == i) continue;
        sumX += MotionEventCompat.getX(ev, i);
        sumY += MotionEventCompat.getY(ev, i);
      }
      final int div = pointerUp ? count - 1 : count;
      final float focusX = sumX / div;
      final float focusY = sumY / div;

      boolean handled = false;

      switch (action & MotionEventCompat.ACTION_MASK) {
        case MotionEventCompat.ACTION_POINTER_DOWN:
          mDownFocusX = mLastFocusX = focusX;
          mDownFocusY = mLastFocusY = focusY;
          // Cancel long press and taps
          cancelTaps();
          break;

        case MotionEventCompat.ACTION_POINTER_UP:
          mDownFocusX = mLastFocusX = focusX;
          mDownFocusY = mLastFocusY = focusY;

          // Check the dot product of current velocities.
          // If the pointer that left was opposing another velocity vector, clear.
          mVelocityTracker.computeCurrentVelocity(1000, mMaximumFlingVelocity);
          final int upIndex = MotionEventCompat.getActionIndex(ev);
          final int id1 = MotionEventCompat.getPointerId(ev, upIndex);
          final float x1 = VelocityTrackerCompat.getXVelocity(mVelocityTracker, id1);
          final float y1 = VelocityTrackerCompat.getYVelocity(mVelocityTracker, id1);
          for (int i = 0; i < count; i++) {
            if (i == upIndex) continue;

            final int id2 = MotionEventCompat.getPointerId(ev, i);
            final float x = x1 * VelocityTrackerCompat.getXVelocity(mVelocityTracker, id2);
            final float y = y1 * VelocityTrackerCompat.getYVelocity(mVelocityTracker, id2);

            final float dot = x + y;
            if (dot < 0) {
              mVelocityTracker.clear();
              break;
            }
          }
          break;

        case MotionEvent.ACTION_DOWN:
          if (mDoubleTapListener != null) {
            boolean hadTapMessage = mHandler.hasMessages(TAP);
            if (hadTapMessage) mHandler.removeMessages(TAP);
            if ((mCurrentDownEvent != null)
                && (mPreviousUpEvent != null)
                && hadTapMessage
                && isConsideredDoubleTap(mCurrentDownEvent, mPreviousUpEvent, ev)) {
              // This is a second tap
              mIsDoubleTapping = true;
              // Give a callback with the first tap of the double-tap
              handled |= mDoubleTapListener.onDoubleTap(mCurrentDownEvent);
              // Give a callback with down event of the double-tap
              handled |= mDoubleTapListener.onDoubleTapEvent(ev);
            } else {
              // This is a first tap
              mHandler.sendEmptyMessageDelayed(TAP, DOUBLE_TAP_TIMEOUT);
            }
          }

          mDownFocusX = mLastFocusX = focusX;
          mDownFocusY = mLastFocusY = focusY;
          if (mCurrentDownEvent != null) {
            mCurrentDownEvent.recycle();
          }
          mCurrentDownEvent = MotionEvent.obtain(ev);
          mAlwaysInTapRegion = true;
          mAlwaysInBiggerTapRegion = true;
          mStillDown = true;
          mInLongPress = false;

          if (mIsLongpressEnabled) {
            mHandler.removeMessages(LONG_PRESS);
            mHandler.sendEmptyMessageAtTime(
                LONG_PRESS, mCurrentDownEvent.getDownTime() + TAP_TIMEOUT + LONGPRESS_TIMEOUT);
          }
          mHandler.sendEmptyMessageAtTime(
              SHOW_PRESS, mCurrentDownEvent.getDownTime() + TAP_TIMEOUT);
          handled |= mListener.onDown(ev);
          break;

        case MotionEvent.ACTION_MOVE:
          if (mInLongPress) {
            break;
          }
          final float scrollX = mLastFocusX - focusX;
          final float scrollY = mLastFocusY - focusY;
          if (mIsDoubleTapping) {
            // Give the move events of the double-tap
            handled |= mDoubleTapListener.onDoubleTapEvent(ev);
          } else if (mAlwaysInTapRegion) {
            final int deltaX = (int) (focusX - mDownFocusX);
            final int deltaY = (int) (focusY - mDownFocusY);
            int distance = (deltaX * deltaX) + (deltaY * deltaY);
            if (distance > mTouchSlopSquare) {
              handled = mListener.onScroll(mCurrentDownEvent, ev, scrollX, scrollY);
              mLastFocusX = focusX;
              mLastFocusY = focusY;
              mAlwaysInTapRegion = false;
              mHandler.removeMessages(TAP);
              mHandler.removeMessages(SHOW_PRESS);
              mHandler.removeMessages(LONG_PRESS);
            }
            if (distance > mTouchSlopSquare) {
              mAlwaysInBiggerTapRegion = false;
            }
          } else if ((Math.abs(scrollX) >= 1) || (Math.abs(scrollY) >= 1)) {
            handled = mListener.onScroll(mCurrentDownEvent, ev, scrollX, scrollY);
            mLastFocusX = focusX;
            mLastFocusY = focusY;
          }
          break;

        case MotionEvent.ACTION_UP:
          mStillDown = false;
          MotionEvent currentUpEvent = MotionEvent.obtain(ev);
          if (mIsDoubleTapping) {
            // Finally, give the up event of the double-tap
            handled |= mDoubleTapListener.onDoubleTapEvent(ev);
          } else if (mInLongPress) {
            mHandler.removeMessages(TAP);
            mInLongPress = false;
          } else if (mAlwaysInTapRegion) {
            handled = mListener.onSingleTapUp(ev);
          } else {

            // A fling must travel the minimum tap distance
            final VelocityTracker velocityTracker = mVelocityTracker;
            final int pointerId = MotionEventCompat.getPointerId(ev, 0);
            velocityTracker.computeCurrentVelocity(1000, mMaximumFlingVelocity);
            final float velocityY = VelocityTrackerCompat.getYVelocity(velocityTracker, pointerId);
            final float velocityX = VelocityTrackerCompat.getXVelocity(velocityTracker, pointerId);

            if ((Math.abs(velocityY) > mMinimumFlingVelocity)
                || (Math.abs(velocityX) > mMinimumFlingVelocity)) {
              handled = mListener.onFling(mCurrentDownEvent, ev, velocityX, velocityY);
            }
          }
          if (mPreviousUpEvent != null) {
            mPreviousUpEvent.recycle();
          }
          // Hold the event we obtained above - listeners may have changed the original.
          mPreviousUpEvent = currentUpEvent;
          if (mVelocityTracker != null) {
            // This may have been cleared when we called out to the
            // application above.
            mVelocityTracker.recycle();
            mVelocityTracker = null;
          }
          mIsDoubleTapping = false;
          mHandler.removeMessages(SHOW_PRESS);
          mHandler.removeMessages(LONG_PRESS);
          break;

        case MotionEvent.ACTION_CANCEL:
          cancel();
          break;
      }

      return handled;
    }

    private void cancel() {
      mHandler.removeMessages(SHOW_PRESS);
      mHandler.removeMessages(LONG_PRESS);
      mHandler.removeMessages(TAP);
      mVelocityTracker.recycle();
      mVelocityTracker = null;
      mIsDoubleTapping = false;
      mStillDown = false;
      mAlwaysInTapRegion = false;
      mAlwaysInBiggerTapRegion = false;
      if (mInLongPress) {
        mInLongPress = false;
      }
    }

    private void cancelTaps() {
      mHandler.removeMessages(SHOW_PRESS);
      mHandler.removeMessages(LONG_PRESS);
      mHandler.removeMessages(TAP);
      mIsDoubleTapping = false;
      mAlwaysInTapRegion = false;
      mAlwaysInBiggerTapRegion = false;
      if (mInLongPress) {
        mInLongPress = false;
      }
    }

    private boolean isConsideredDoubleTap(
        MotionEvent firstDown, MotionEvent firstUp, MotionEvent secondDown) {
      if (!mAlwaysInBiggerTapRegion) {
        return false;
      }

      if (secondDown.getEventTime() - firstUp.getEventTime() > DOUBLE_TAP_TIMEOUT) {
        return false;
      }

      int deltaX = (int) firstDown.getX() - (int) secondDown.getX();
      int deltaY = (int) firstDown.getY() - (int) secondDown.getY();
      return (deltaX * deltaX + deltaY * deltaY < mDoubleTapSlopSquare);
    }

    private void dispatchLongPress() {
      mHandler.removeMessages(TAP);
      mInLongPress = true;
      mListener.onLongPress(mCurrentDownEvent);
    }
  }
  @Override
  public boolean onTouchEvent(MotionEvent ev) {
    final int action = ev.getAction();
    switch (action & MotionEvent.ACTION_MASK) {
      case MotionEvent.ACTION_DOWN:
        if (mPendingCheckForTap == null) {
          mPendingCheckForTap = new CheckForHeaderTap();
        }
        postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());

        final int y = (int) ev.getY();
        mMotionY = y;
        mMotionHeaderPosition = findMotionHeader(y);
        if (mMotionHeaderPosition == NO_MATCHED_HEADER || mScrollState == SCROLL_STATE_FLING) {
          // Don't consume the event and pass it to super because we
          // can't handle it yet.
          break;
        }
        mTouchMode = TOUCH_MODE_DOWN;
        return true;
      case MotionEvent.ACTION_MOVE:
        if (mMotionHeaderPosition != NO_MATCHED_HEADER
            && Math.abs(ev.getY() - mMotionY) > mTouchSlop) {
          // Detected scroll initiation so cancel touch completion on
          // header.
          mTouchMode = TOUCH_MODE_REST;
          final View header = getHeaderAt(mMotionHeaderPosition);
          if (header != null) {
            header.setPressed(false);
          }
          final Handler handler = getHandler();
          if (handler != null) {
            handler.removeCallbacks(mPendingCheckForLongPress);
          }
          mMotionHeaderPosition = NO_MATCHED_HEADER;
        }
        break;
      case MotionEvent.ACTION_UP:
        if (mTouchMode == TOUCH_MODE_FINISHED_LONG_PRESS) {
          return true;
        }
        if (mTouchMode == TOUCH_MODE_REST || mMotionHeaderPosition == NO_MATCHED_HEADER) {
          break;
        }

        final View header = getHeaderAt(mMotionHeaderPosition);
        if (header != null && !header.hasFocusable()) {
          if (mTouchMode != TOUCH_MODE_DOWN) {
            header.setPressed(false);
          }

          if (mPerformHeaderClick == null) {
            mPerformHeaderClick = new PerformHeaderClick();
          }

          final PerformHeaderClick performHeaderClick = mPerformHeaderClick;
          performHeaderClick.mClickMotionPosition = mMotionHeaderPosition;
          performHeaderClick.rememberWindowAttachCount();

          if (mTouchMode != TOUCH_MODE_DOWN || mTouchMode != TOUCH_MODE_TAP) {
            final Handler handler = getHandler();
            if (handler != null) {
              handler.removeCallbacks(
                  mTouchMode == TOUCH_MODE_DOWN ? mPendingCheckForTap : mPendingCheckForLongPress);
            }

            if (!mDataChanged) {
              // Got here so must be a tap. The long press would
              // have trigger on the callback handler. Probably.
              mTouchMode = TOUCH_MODE_TAP;
              header.setPressed(true);
              setPressed(true);
              if (mTouchModeReset != null) {
                removeCallbacks(mTouchModeReset);
              }
              mTouchModeReset =
                  new Runnable() {
                    @Override
                    public void run() {
                      mTouchMode = TOUCH_MODE_REST;
                      header.setPressed(false);
                      setPressed(false);
                      if (!mDataChanged) {
                        performHeaderClick.run();
                      }
                    }
                  };
              postDelayed(mTouchModeReset, ViewConfiguration.getPressedStateDuration());
            } else {
              mTouchMode = TOUCH_MODE_REST;
            }
          } else if (!mDataChanged) {
            performHeaderClick.run();
          }
        }
        mTouchMode = TOUCH_MODE_REST;
        return true;
    }
    return super.onTouchEvent(ev);
  }
  @Override
  public boolean onTouchEvent(MotionEvent event) {
    boolean superOnTouchEvent = super.onTouchEvent(event);

    if (!isEnabled() || !childView.isEnabled()) return superOnTouchEvent;

    boolean isEventInBounds = bounds.contains((int) event.getX(), (int) event.getY());

    if (isEventInBounds) {
      previousCoords.set(currentCoords.x, currentCoords.y);
      currentCoords.set((int) event.getX(), (int) event.getY());
    }

    boolean gestureResult = gestureDetector.onTouchEvent(event);
    if (gestureResult || mHasPerformedLongPress) {
      return true;
    } else {
      int action = event.getActionMasked();
      switch (action) {
        case MotionEvent.ACTION_UP:
          pendingClickEvent = new PerformClickEvent();

          if (prePressed) {
            childView.setPressed(true);
            postDelayed(
                new Runnable() {
                  @Override
                  public void run() {
                    childView.setPressed(false);
                  }
                },
                ViewConfiguration.getPressedStateDuration());
          }

          if (isEventInBounds) {
            startRipple(pendingClickEvent);
          } else if (!rippleHover) {
            setRadius(0);
          }
          if (!rippleDelayClick && isEventInBounds) {
            pendingClickEvent.run();
          }
          cancelPressedEvent();
          break;
        case MotionEvent.ACTION_DOWN:
          setPositionInAdapter();
          eventCancelled = false;
          pendingPressEvent = new PressedEvent(event);
          if (isInScrollingContainer()) {
            cancelPressedEvent();
            prePressed = true;
            postDelayed(pendingPressEvent, ViewConfiguration.getTapTimeout());
          } else {
            pendingPressEvent.run();
          }
          break;
        case MotionEvent.ACTION_CANCEL:
          if (rippleInAdapter) {
            // dont use current coords in adapter since they tend to jump drastically on scroll
            currentCoords.set(previousCoords.x, previousCoords.y);
            previousCoords = new Point();
          }
          childView.onTouchEvent(event);
          if (rippleHover) {
            if (!prePressed) {
              startRipple(null);
            }
          } else {
            childView.setPressed(false);
          }
          cancelPressedEvent();
          break;
        case MotionEvent.ACTION_MOVE:
          if (rippleHover) {
            if (isEventInBounds && !eventCancelled) {
              invalidate();
            } else if (!isEventInBounds) {
              startRipple(null);
            }
          }

          if (!isEventInBounds) {
            cancelPressedEvent();
            if (hoverAnimator != null) {
              hoverAnimator.cancel();
            }
            childView.onTouchEvent(event);
            eventCancelled = true;
          }
          break;
      }
      return true;
    }
  }
Beispiel #10
0
 private static void performTouchAction(
     @NonNull final ViewGroup container,
     @NonNull String action,
     @NonNull final MotionEvent event) {
   if (action.equals(ACTION_DOUBLE_TAP)) {
     // TODO: use input command?
     injectMotionEvent(container, event, MotionEvent.ACTION_DOWN);
     injectMotionEvent(container, event, MotionEvent.ACTION_UP);
     injectMotionEvent(container, event, MotionEvent.ACTION_DOWN);
     injectMotionEvent(container, event, MotionEvent.ACTION_UP);
   } else if (action.equals(ACTION_LONG_PRESS)) {
     // TODO: use input command?
     injectMotionEventForLongPress(container, event, MotionEvent.ACTION_DOWN);
     injectMotionEvent(container, event, MotionEvent.ACTION_CANCEL);
   } else if (action.equals(ACTION_LONG_PRESS_FULL)) {
     // TODO: use input command?
     injectMotionEvent(container, event, MotionEvent.ACTION_DOWN);
     container.postDelayed(
         new Runnable() {
           @Override
           public void run() {
             injectMotionEvent(container, event, MotionEvent.ACTION_UP);
           }
         },
         ViewConfiguration.getLongPressTimeout() + ViewConfiguration.getTapTimeout());
   } else if (action.equals(ACTION_SCROLL_UP)) {
     View view =
         findViewAtPosition(
             container,
             Math.round(event.getX()),
             Math.round(event.getY()),
             new OnViewFoundListener() {
               @Override
               public boolean onViewFound(View view) {
                 if (view.canScrollVertically(-1)) {
                   if (view instanceof AbsListView) {
                     ((AbsListView) view).smoothScrollToPosition(0);
                   } else if (view instanceof RecyclerView) {
                     ((RecyclerView) view).smoothScrollToPosition(0);
                   } else if (view instanceof ScrollView) {
                     ((ScrollView) view).fullScroll(View.FOCUS_UP);
                   } else {
                     view.scrollTo(view.getScrollX(), 0);
                   }
                   return true;
                 }
                 return false;
               }
             });
   } else if (action.equals(ACTION_SCROLL_DOWN)) {
     View view =
         findViewAtPosition(
             container,
             Math.round(event.getX()),
             Math.round(event.getY()),
             new OnViewFoundListener() {
               @Override
               public boolean onViewFound(View view) {
                 // TODO: doesn't work...
                 if (view.canScrollVertically(1)) {
                   if (view instanceof AbsListView) {
                     ((AbsListView) view)
                         .smoothScrollToPosition(((AbsListView) view).getChildCount() - 1);
                   } else if (view instanceof RecyclerView) {
                     ((RecyclerView) view)
                         .smoothScrollToPosition(((RecyclerView) view).getChildCount() - 1);
                   } else if (view instanceof ScrollView) {
                     ((ScrollView) view).fullScroll(View.FOCUS_DOWN);
                   } else {
                     view.scrollTo(view.getScrollX(), view.getBottom());
                   }
                   return true;
                 }
                 return false;
               }
             });
   }
 }
  @Override
  public boolean onTouchEvent(MotionEvent event) {
    int action = event.getActionMasked();
    final boolean pointerUp = action == MotionEvent.ACTION_POINTER_UP;
    final int skipIndex = pointerUp ? event.getActionIndex() : -1;

    // Determine focal point
    float sumX = 0, sumY = 0;
    final int count = event.getPointerCount();
    for (int i = 0; i < count; i++) {
      if (skipIndex == i) continue;
      sumX += event.getX(i);
      sumY += event.getY(i);
    }
    final int div = pointerUp ? count - 1 : count;
    float x = sumX / div;
    float y = sumY / div;

    if (action == MotionEvent.ACTION_DOWN) {
      mFirstX = x;
      mFirstY = y;
      mTouchDownTime = System.currentTimeMillis();
      if (mTouchCallback != null) {
        mTouchCallback.onTouchDown();
      }
    } else if (action == MotionEvent.ACTION_UP) {
      ViewConfiguration config = ViewConfiguration.get(getContext());

      float squaredDist = (mFirstX - x) * (mFirstX - x) + (mFirstY - y) * (mFirstY - y);
      float slop = config.getScaledTouchSlop() * config.getScaledTouchSlop();
      long now = System.currentTimeMillis();
      if (mTouchCallback != null) {
        // only do this if it's a small movement
        if (squaredDist < slop && now < mTouchDownTime + ViewConfiguration.getTapTimeout()) {
          mTouchCallback.onTap();
        }
        mTouchCallback.onTouchUp();
      }
    }

    if (!mTouchEnabled) {
      return true;
    }

    synchronized (mLock) {
      mScaleGestureDetector.onTouchEvent(event);
      switch (action) {
        case MotionEvent.ACTION_MOVE:
          float[] point = mTempPoint;
          point[0] = (mLastX - x) / mRenderer.scale;
          point[1] = (mLastY - y) / mRenderer.scale;
          mInverseRotateMatrix.mapPoints(point);
          mCenterX += point[0];
          mCenterY += point[1];
          updateCenter();
          invalidate();
          break;
      }
      if (mRenderer.source != null) {
        // Adjust position so that the wallpaper covers the entire area
        // of the screen
        final RectF edges = mTempEdges;
        getEdgesHelper(edges);
        final float scale = mRenderer.scale;

        float[] coef = mTempCoef;
        coef[0] = 1;
        coef[1] = 1;
        mRotateMatrix.mapPoints(coef);
        float[] adjustment = mTempAdjustment;
        mTempAdjustment[0] = 0;
        mTempAdjustment[1] = 0;
        if (edges.left > 0) {
          adjustment[0] = edges.left / scale;
        } else if (edges.right < getWidth()) {
          adjustment[0] = (edges.right - getWidth()) / scale;
        }
        if (edges.top > 0) {
          adjustment[1] = FloatMath.ceil(edges.top / scale);
        } else if (edges.bottom < getHeight()) {
          adjustment[1] = (edges.bottom - getHeight()) / scale;
        }
        for (int dim = 0; dim <= 1; dim++) {
          if (coef[dim] > 0) adjustment[dim] = FloatMath.ceil(adjustment[dim]);
        }

        mInverseRotateMatrix.mapPoints(adjustment);
        mCenterX += adjustment[0];
        mCenterY += adjustment[1];
        updateCenter();
      }
    }

    mLastX = x;
    mLastY = y;
    return true;
  }
/**
 * Perform asynchronous dispatch of input events in a {@link WebView}.
 *
 * <p>This dispatcher is shared by the UI thread ({@link WebViewClassic}) and web kit thread ({@link
 * WebViewCore}). The UI thread enqueues events for processing, waits for the web kit thread to
 * handle them, and then performs additional processing depending on the outcome.
 *
 * <p>How it works:
 *
 * <p>1. The web view thread receives an input event from the input system on the UI thread in its
 * {@link WebViewClassic#onTouchEvent} handler. It sends the input event to the dispatcher, then
 * immediately returns true to the input system to indicate that it will handle the event.
 *
 * <p>2. The web kit thread is notified that an event has been enqueued. Meanwhile additional events
 * may be enqueued from the UI thread. In some cases, the dispatcher may decide to coalesce motion
 * events into larger batches or to cancel events that have been sitting in the queue for too long.
 *
 * <p>3. The web kit thread wakes up and handles all input events that are waiting for it. After
 * processing each input event, it informs the dispatcher whether the web application has decided to
 * handle the event itself and to prevent default event handling.
 *
 * <p>4. If web kit indicates that it wants to prevent default event handling, then web kit consumes
 * the remainder of the gesture and web view receives a cancel event if needed. Otherwise, the web
 * view handles the gesture on the UI thread normally.
 *
 * <p>5. If the web kit thread takes too long to handle an input event, then it loses the right to
 * handle it. The dispatcher synthesizes a cancellation event for web kit and then tells the web
 * view on the UI thread to handle the event that timed out along with the rest of the gesture.
 *
 * <p>One thing to keep in mind about the dispatcher is that what goes into the dispatcher is not
 * necessarily what the web kit or UI thread will see. As mentioned above, the dispatcher may tweak
 * the input event stream to improve responsiveness. Both web view and web kit are guaranteed to
 * perceive a consistent stream of input events but they might not always see the same events
 * (especially if one decides to prevent the other from handling a particular gesture).
 *
 * <p>This implementation very deliberately does not refer to the {@link WebViewClassic} or {@link
 * WebViewCore} classes, preferring to communicate with them only via interfaces to avoid
 * unintentional coupling to their implementation details.
 *
 * <p>Currently, the input dispatcher only handles pointer events (includes touch, hover and scroll
 * events). In principle, it could be extended to handle trackball and key events if needed.
 *
 * @hide
 */
final class WebViewInputDispatcher {
  private static final String TAG = "WebViewInputDispatcher";
  private static final boolean DEBUG = false;
  // This enables batching of MotionEvents. It will combine multiple MotionEvents
  // together into a single MotionEvent if more events come in while we are
  // still waiting on the processing of a previous event.
  // If this is set to false, we will instead opt to drop ACTION_MOVE
  // events we cannot keep up with.
  // TODO: If batching proves to be working well, remove this
  private static final boolean ENABLE_EVENT_BATCHING = true;

  private final Object mLock = new Object();

  // Pool of queued input events.  (guarded by mLock)
  private static final int MAX_DISPATCH_EVENT_POOL_SIZE = 10;
  private DispatchEvent mDispatchEventPool;
  private int mDispatchEventPoolSize;

  // Posted state, tracks events posted to the dispatcher.  (guarded by mLock)
  private final TouchStream mPostTouchStream = new TouchStream();
  private boolean mPostSendTouchEventsToWebKit;
  private boolean mPostDoNotSendTouchEventsToWebKitUntilNextGesture;
  private boolean mPostLongPressScheduled;
  private boolean mPostClickScheduled;
  private boolean mPostShowTapHighlightScheduled;
  private boolean mPostHideTapHighlightScheduled;
  private int mPostLastWebKitXOffset;
  private int mPostLastWebKitYOffset;
  private float mPostLastWebKitScale;

  // State for event tracking (click, longpress, double tap, etc..)
  private boolean mIsDoubleTapCandidate;
  private boolean mIsTapCandidate;
  private float mInitialDownX;
  private float mInitialDownY;
  private float mTouchSlopSquared;
  private float mDoubleTapSlopSquared;

  // Web kit state, tracks events observed by web kit.  (guarded by mLock)
  private final DispatchEventQueue mWebKitDispatchEventQueue = new DispatchEventQueue();
  private final TouchStream mWebKitTouchStream = new TouchStream();
  private final WebKitCallbacks mWebKitCallbacks;
  private final WebKitHandler mWebKitHandler;
  private boolean mWebKitDispatchScheduled;
  private boolean mWebKitTimeoutScheduled;
  private long mWebKitTimeoutTime;
  /// M: Web kit state, tracks events prevent by web kit.
  private final PreventTouchStream mWebKitPreventTouchStream = new PreventTouchStream();

  // UI state, tracks events observed by the UI.  (guarded by mLock)
  private final DispatchEventQueue mUiDispatchEventQueue = new DispatchEventQueue();
  private final TouchStream mUiTouchStream = new TouchStream();
  private final UiCallbacks mUiCallbacks;
  private final UiHandler mUiHandler;
  private boolean mUiDispatchScheduled;

  // Give up on web kit handling of input events when this timeout expires.
  private static final long WEBKIT_TIMEOUT_MILLIS = 200;
  private static final int TAP_TIMEOUT = ViewConfiguration.getTapTimeout();
  private static final int LONG_PRESS_TIMEOUT =
      ViewConfiguration.getLongPressTimeout() + TAP_TIMEOUT;
  private static final int DOUBLE_TAP_TIMEOUT = ViewConfiguration.getDoubleTapTimeout();
  private static final int PRESSED_STATE_DURATION = ViewConfiguration.getPressedStateDuration();

  /**
   * Event type: Indicates a touch event type.
   *
   * <p>This event is delivered together with a {@link MotionEvent} with one of the following
   * actions: {@link MotionEvent#ACTION_DOWN}, {@link MotionEvent#ACTION_MOVE}, {@link
   * MotionEvent#ACTION_UP}, {@link MotionEvent#ACTION_POINTER_DOWN}, {@link
   * MotionEvent#ACTION_POINTER_UP}, {@link MotionEvent#ACTION_CANCEL}.
   */
  public static final int EVENT_TYPE_TOUCH = 0;

  /**
   * Event type: Indicates a hover event type.
   *
   * <p>This event is delivered together with a {@link MotionEvent} with one of the following
   * actions: {@link MotionEvent#ACTION_HOVER_ENTER}, {@link MotionEvent#ACTION_HOVER_MOVE}, {@link
   * MotionEvent#ACTION_HOVER_MOVE}.
   */
  public static final int EVENT_TYPE_HOVER = 1;

  /**
   * Event type: Indicates a scroll event type.
   *
   * <p>This event is delivered together with a {@link MotionEvent} with action {@link
   * MotionEvent#ACTION_SCROLL}.
   */
  public static final int EVENT_TYPE_SCROLL = 2;

  /**
   * Event type: Indicates a long-press event type.
   *
   * <p>This event is delivered in the middle of a sequence of {@link #EVENT_TYPE_TOUCH} events. It
   * includes a {@link MotionEvent} with action {@link MotionEvent#ACTION_MOVE} that indicates the
   * current touch coordinates of the long-press.
   *
   * <p>This event is sent when the current touch gesture has been held longer than the long-press
   * interval.
   */
  public static final int EVENT_TYPE_LONG_PRESS = 3;

  /**
   * Event type: Indicates a click event type.
   *
   * <p>This event is delivered after a sequence of {@link #EVENT_TYPE_TOUCH} events that comprise a
   * complete gesture ending with {@link MotionEvent#ACTION_UP}. It includes a {@link MotionEvent}
   * with action {@link MotionEvent#ACTION_UP} that indicates the location of the click.
   *
   * <p>This event is sent shortly after the end of a touch after the double-tap interval has
   * expired to indicate a click.
   */
  public static final int EVENT_TYPE_CLICK = 4;

  /**
   * Event type: Indicates a double-tap event type.
   *
   * <p>This event is delivered after a sequence of {@link #EVENT_TYPE_TOUCH} events that comprise a
   * complete gesture ending with {@link MotionEvent#ACTION_UP}. It includes a {@link MotionEvent}
   * with action {@link MotionEvent#ACTION_UP} that indicates the location of the double-tap.
   *
   * <p>This event is sent immediately after a sequence of two touches separated in time by no more
   * than the double-tap interval and separated in space by no more than the double-tap slop.
   */
  public static final int EVENT_TYPE_DOUBLE_TAP = 5;

  /** Event type: Indicates that a hit test should be performed */
  public static final int EVENT_TYPE_HIT_TEST = 6;

  /** Flag: This event is private to this queue. Do not forward it. */
  public static final int FLAG_PRIVATE = 1 << 0;

  /**
   * Flag: This event is currently being processed by web kit. If a timeout occurs, make a copy of
   * it before forwarding the event to another queue.
   */
  public static final int FLAG_WEBKIT_IN_PROGRESS = 1 << 1;

  /** Flag: A timeout occurred while waiting for web kit to process this input event. */
  public static final int FLAG_WEBKIT_TIMEOUT = 1 << 2;

  /**
   * Flag: Indicates that the event was transformed for delivery to web kit. The event must be
   * transformed back before being delivered to the UI.
   */
  public static final int FLAG_WEBKIT_TRANSFORMED_EVENT = 1 << 3;

  public WebViewInputDispatcher(UiCallbacks uiCallbacks, WebKitCallbacks webKitCallbacks) {
    this.mUiCallbacks = uiCallbacks;
    mUiHandler = new UiHandler(uiCallbacks.getUiLooper());

    this.mWebKitCallbacks = webKitCallbacks;
    mWebKitHandler = new WebKitHandler(webKitCallbacks.getWebKitLooper());

    ViewConfiguration config = ViewConfiguration.get(mUiCallbacks.getContext());
    mDoubleTapSlopSquared = config.getScaledDoubleTapSlop();
    mDoubleTapSlopSquared = (mDoubleTapSlopSquared * mDoubleTapSlopSquared);
    mTouchSlopSquared = config.getScaledTouchSlop();
    mTouchSlopSquared = (mTouchSlopSquared * mTouchSlopSquared);
  }

  /**
   * Sets whether web kit wants to receive touch events.
   *
   * @param enable True to enable dispatching of touch events to web kit, otherwise web kit will be
   *     skipped.
   */
  public void setWebKitWantsTouchEvents(boolean enable) {
    if (DEBUG) {
      Log.d(TAG, "webkitWantsTouchEvents: " + enable);
    }
    synchronized (mLock) {
      if (mPostSendTouchEventsToWebKit != enable) {
        if (!enable) {
          enqueueWebKitCancelTouchEventIfNeededLocked();
        }
        mPostSendTouchEventsToWebKit = enable;
      }
    }
  }

  /**
   * Posts a pointer event to the dispatch queue.
   *
   * @param event The event to post.
   * @param webKitXOffset X offset to apply to events before dispatching them to web kit.
   * @param webKitYOffset Y offset to apply to events before dispatching them to web kit.
   * @param webKitScale The scale factor to apply to translated events before dispatching them to
   *     web kit.
   * @return True if the dispatcher will handle the event, false if the event is unsupported.
   */
  public boolean postPointerEvent(
      MotionEvent event, int webKitXOffset, int webKitYOffset, float webKitScale) {
    if (event == null) {
      throw new IllegalArgumentException("event cannot be null");
    }

    if (DEBUG) {
      Log.d(TAG, "postPointerEvent: " + event);
    }

    final int action = event.getActionMasked();
    final int eventType;
    switch (action) {
      case MotionEvent.ACTION_DOWN:
      case MotionEvent.ACTION_MOVE:
      case MotionEvent.ACTION_UP:
      case MotionEvent.ACTION_POINTER_DOWN:
      case MotionEvent.ACTION_POINTER_UP:
      case MotionEvent.ACTION_CANCEL:
        eventType = EVENT_TYPE_TOUCH;
        break;
      case MotionEvent.ACTION_SCROLL:
        eventType = EVENT_TYPE_SCROLL;
        break;
      case MotionEvent.ACTION_HOVER_ENTER:
      case MotionEvent.ACTION_HOVER_MOVE:
      case MotionEvent.ACTION_HOVER_EXIT:
        eventType = EVENT_TYPE_HOVER;
        break;
      default:
        return false; // currently unsupported event type
    }

    synchronized (mLock) {
      // Ensure that the event is consistent and should be delivered.
      MotionEvent eventToEnqueue = event;
      if (eventType == EVENT_TYPE_TOUCH) {
        eventToEnqueue = mPostTouchStream.update(event);
        if (eventToEnqueue == null) {
          if (DEBUG) {
            Log.d(TAG, "postPointerEvent: dropped event " + event);
          }
          unscheduleLongPressLocked();
          unscheduleClickLocked();
          hideTapCandidateLocked();
          return false;
        }

        if (action == MotionEvent.ACTION_DOWN && mPostSendTouchEventsToWebKit) {
          if (mUiCallbacks.shouldInterceptTouchEvent(eventToEnqueue)) {
            mPostDoNotSendTouchEventsToWebKitUntilNextGesture = true;
          } else if (mPostDoNotSendTouchEventsToWebKitUntilNextGesture) {
            // Recover from a previous web kit timeout.
            mPostDoNotSendTouchEventsToWebKitUntilNextGesture = false;
          }
        }
      }

      // Copy the event because we need to retain ownership.
      if (eventToEnqueue == event) {
        eventToEnqueue = event.copy();
      }

      DispatchEvent d =
          obtainDispatchEventLocked(
              eventToEnqueue, eventType, 0, webKitXOffset, webKitYOffset, webKitScale);
      updateStateTrackersLocked(d, event);
      enqueueEventLocked(d);
    }
    return true;
  }

  private void scheduleLongPressLocked() {
    unscheduleLongPressLocked();
    mPostLongPressScheduled = true;
    mUiHandler.sendEmptyMessageDelayed(UiHandler.MSG_LONG_PRESS, LONG_PRESS_TIMEOUT);
  }

  private void unscheduleLongPressLocked() {
    if (mPostLongPressScheduled) {
      mPostLongPressScheduled = false;
      mUiHandler.removeMessages(UiHandler.MSG_LONG_PRESS);
    }
  }

  private void postLongPress() {
    synchronized (mLock) {
      if (!mPostLongPressScheduled) {
        return;
      }
      mPostLongPressScheduled = false;

      MotionEvent event = mPostTouchStream.getLastEvent();
      if (event == null) {
        return;
      }

      switch (event.getActionMasked()) {
        case MotionEvent.ACTION_DOWN:
        case MotionEvent.ACTION_MOVE:
        case MotionEvent.ACTION_POINTER_DOWN:
        case MotionEvent.ACTION_POINTER_UP:
          break;
        default:
          return;
      }

      MotionEvent eventToEnqueue = MotionEvent.obtainNoHistory(event);
      eventToEnqueue.setAction(MotionEvent.ACTION_MOVE);
      DispatchEvent d =
          obtainDispatchEventLocked(
              eventToEnqueue,
              EVENT_TYPE_LONG_PRESS,
              0,
              mPostLastWebKitXOffset,
              mPostLastWebKitYOffset,
              mPostLastWebKitScale);
      enqueueEventLocked(d);
    }
  }

  private void hideTapCandidateLocked() {
    unscheduleHideTapHighlightLocked();
    unscheduleShowTapHighlightLocked();
    mUiCallbacks.showTapHighlight(false);
  }

  private void showTapCandidateLocked() {
    unscheduleHideTapHighlightLocked();
    unscheduleShowTapHighlightLocked();
    mUiCallbacks.showTapHighlight(true);
  }

  private void scheduleShowTapHighlightLocked() {
    unscheduleShowTapHighlightLocked();
    mPostShowTapHighlightScheduled = true;
    mUiHandler.sendEmptyMessageDelayed(UiHandler.MSG_SHOW_TAP_HIGHLIGHT, TAP_TIMEOUT);
  }

  private void unscheduleShowTapHighlightLocked() {
    if (mPostShowTapHighlightScheduled) {
      mPostShowTapHighlightScheduled = false;
      mUiHandler.removeMessages(UiHandler.MSG_SHOW_TAP_HIGHLIGHT);
    }
  }

  private void scheduleHideTapHighlightLocked() {
    unscheduleHideTapHighlightLocked();
    mPostHideTapHighlightScheduled = true;
    mUiHandler.sendEmptyMessageDelayed(UiHandler.MSG_HIDE_TAP_HIGHLIGHT, PRESSED_STATE_DURATION);
  }

  private void unscheduleHideTapHighlightLocked() {
    if (mPostHideTapHighlightScheduled) {
      mPostHideTapHighlightScheduled = false;
      mUiHandler.removeMessages(UiHandler.MSG_HIDE_TAP_HIGHLIGHT);
    }
  }

  private void postShowTapHighlight(boolean show) {
    synchronized (mLock) {
      if (show) {
        if (!mPostShowTapHighlightScheduled) {
          return;
        }
        mPostShowTapHighlightScheduled = false;
      } else {
        if (!mPostHideTapHighlightScheduled) {
          return;
        }
        mPostHideTapHighlightScheduled = false;
      }
      mUiCallbacks.showTapHighlight(show);
    }
  }

  private void scheduleClickLocked() {
    unscheduleClickLocked();
    mPostClickScheduled = true;
    mUiHandler.sendEmptyMessageDelayed(UiHandler.MSG_CLICK, DOUBLE_TAP_TIMEOUT);
  }

  private void unscheduleClickLocked() {
    if (mPostClickScheduled) {
      mPostClickScheduled = false;
      mUiHandler.removeMessages(UiHandler.MSG_CLICK);
    }
  }

  private void postClick() {
    synchronized (mLock) {
      if (!mPostClickScheduled) {
        return;
      }
      mPostClickScheduled = false;

      MotionEvent event = mPostTouchStream.getLastEvent();
      if (event == null || event.getAction() != MotionEvent.ACTION_UP) {
        return;
      }

      showTapCandidateLocked();
      MotionEvent eventToEnqueue = MotionEvent.obtainNoHistory(event);
      DispatchEvent d =
          obtainDispatchEventLocked(
              eventToEnqueue,
              EVENT_TYPE_CLICK,
              0,
              mPostLastWebKitXOffset,
              mPostLastWebKitYOffset,
              mPostLastWebKitScale);
      enqueueEventLocked(d);
    }
  }

  private void checkForDoubleTapOnDownLocked(MotionEvent event) {
    mIsDoubleTapCandidate = false;
    if (!mPostClickScheduled) {
      return;
    }
    int deltaX = (int) mInitialDownX - (int) event.getX();
    int deltaY = (int) mInitialDownY - (int) event.getY();
    if ((deltaX * deltaX + deltaY * deltaY) < mDoubleTapSlopSquared) {
      unscheduleClickLocked();
      mIsDoubleTapCandidate = true;
    }
  }

  private boolean isClickCandidateLocked(MotionEvent event) {
    if (event == null || event.getActionMasked() != MotionEvent.ACTION_UP || !mIsTapCandidate) {
      return false;
    }
    long downDuration = event.getEventTime() - event.getDownTime();
    return downDuration < LONG_PRESS_TIMEOUT;
  }

  private void enqueueDoubleTapLocked(MotionEvent event) {
    MotionEvent eventToEnqueue = MotionEvent.obtainNoHistory(event);
    DispatchEvent d =
        obtainDispatchEventLocked(
            eventToEnqueue,
            EVENT_TYPE_DOUBLE_TAP,
            0,
            mPostLastWebKitXOffset,
            mPostLastWebKitYOffset,
            mPostLastWebKitScale);
    enqueueEventLocked(d);
  }

  private void enqueueHitTestLocked(MotionEvent event) {
    mUiCallbacks.clearPreviousHitTest();
    MotionEvent eventToEnqueue = MotionEvent.obtainNoHistory(event);
    DispatchEvent d =
        obtainDispatchEventLocked(
            eventToEnqueue,
            EVENT_TYPE_HIT_TEST,
            0,
            mPostLastWebKitXOffset,
            mPostLastWebKitYOffset,
            mPostLastWebKitScale);
    enqueueEventLocked(d);
  }

  private void checkForSlopLocked(MotionEvent event) {
    if (!mIsTapCandidate) {
      return;
    }
    int deltaX = (int) mInitialDownX - (int) event.getX();
    int deltaY = (int) mInitialDownY - (int) event.getY();
    if ((deltaX * deltaX + deltaY * deltaY) > mTouchSlopSquared) {
      unscheduleLongPressLocked();
      mIsTapCandidate = false;
      hideTapCandidateLocked();
    }
  }

  private void updateStateTrackersLocked(DispatchEvent d, MotionEvent event) {
    mPostLastWebKitXOffset = d.mWebKitXOffset;
    mPostLastWebKitYOffset = d.mWebKitYOffset;
    mPostLastWebKitScale = d.mWebKitScale;
    int action = event != null ? event.getAction() : MotionEvent.ACTION_CANCEL;
    if (d.mEventType != EVENT_TYPE_TOUCH) {
      return;
    }

    if (action == MotionEvent.ACTION_CANCEL || event.getPointerCount() > 1) {
      unscheduleLongPressLocked();
      unscheduleClickLocked();
      hideTapCandidateLocked();
      mIsDoubleTapCandidate = false;
      mIsTapCandidate = false;
      hideTapCandidateLocked();
    } else if (action == MotionEvent.ACTION_DOWN) {
      checkForDoubleTapOnDownLocked(event);
      scheduleLongPressLocked();
      mIsTapCandidate = true;
      mInitialDownX = event.getX();
      mInitialDownY = event.getY();
      enqueueHitTestLocked(event);
      if (mIsDoubleTapCandidate) {
        hideTapCandidateLocked();
      } else {
        scheduleShowTapHighlightLocked();
      }
    } else if (action == MotionEvent.ACTION_UP) {
      unscheduleLongPressLocked();
      if (isClickCandidateLocked(event)) {
        if (mIsDoubleTapCandidate) {
          hideTapCandidateLocked();
          enqueueDoubleTapLocked(event);
        } else {
          scheduleClickLocked();
        }
      } else {
        hideTapCandidateLocked();
      }
    } else if (action == MotionEvent.ACTION_MOVE) {
      checkForSlopLocked(event);
    }
  }

  /**
   * Dispatches pending web kit events. Must only be called from the web kit thread.
   *
   * <p>This method may be used to flush the queue of pending input events immediately. This method
   * may help to reduce input dispatch latency if called before certain expensive operations such as
   * drawing.
   */
  public void dispatchWebKitEvents() {
    dispatchWebKitEvents(false);
  }

  private void dispatchWebKitEvents(boolean calledFromHandler) {
    for (; ; ) {
      // Get the next event, but leave it in the queue so we can move it to the UI
      // queue if a timeout occurs.
      DispatchEvent d;
      MotionEvent event;
      final int eventType;
      int flags;
      synchronized (mLock) {
        if (!ENABLE_EVENT_BATCHING) {
          drainStaleWebKitEventsLocked();
        }
        d = mWebKitDispatchEventQueue.mHead;
        if (d == null) {
          if (mWebKitDispatchScheduled) {
            mWebKitDispatchScheduled = false;
            if (!calledFromHandler) {
              mWebKitHandler.removeMessages(WebKitHandler.MSG_DISPATCH_WEBKIT_EVENTS);
            }
          }
          return;
        }

        event = d.mEvent;
        if (event != null) {
          event.offsetLocation(d.mWebKitXOffset, d.mWebKitYOffset);
          event.scale(d.mWebKitScale);
          d.mFlags |= FLAG_WEBKIT_TRANSFORMED_EVENT;
        }

        eventType = d.mEventType;
        if (eventType == EVENT_TYPE_TOUCH) {
          event = mWebKitTouchStream.update(event);
          if (DEBUG && event == null && d.mEvent != null) {
            Log.d(TAG, "dispatchWebKitEvents: dropped event " + d.mEvent);
          }
        }

        d.mFlags |= FLAG_WEBKIT_IN_PROGRESS;
        flags = d.mFlags;
      }

      // Handle the event.
      final boolean preventDefault;
      if (event == null) {
        preventDefault = false;
      } else {
        preventDefault = dispatchWebKitEvent(event, eventType, flags);
      }

      synchronized (mLock) {
        /// M: update last event prevent status. @ {
        boolean lastPreventDefault = false;
        if (d.mEventType == EVENT_TYPE_TOUCH) {
          lastPreventDefault = mWebKitPreventTouchStream.update(event, preventDefault);
        }
        /// @ }
        flags = d.mFlags;
        d.mFlags = flags & ~FLAG_WEBKIT_IN_PROGRESS;
        boolean recycleEvent = event != d.mEvent;

        if ((flags & FLAG_WEBKIT_TIMEOUT) != 0) {
          // A timeout occurred!
          recycleDispatchEventLocked(d);
        } else {
          // Web kit finished in a timely manner.  Dequeue the event.
          assert mWebKitDispatchEventQueue.mHead == d;
          mWebKitDispatchEventQueue.dequeue();

          updateWebKitTimeoutLocked();

          if ((flags & FLAG_PRIVATE) != 0) {
            // Event was intended for web kit only.  All done.
            recycleDispatchEventLocked(d);
          } else if (preventDefault) {
            // Web kit has decided to consume the event!
            if (d.mEventType == EVENT_TYPE_TOUCH) {
              /// M: Consider two consistent events is preventDefault. @ {
              if (lastPreventDefault) {
                Log.d(TAG, "Webkit prevent current and last event, cancel ui event");
                enqueueUiCancelTouchEventIfNeededLocked();
              } else {
                Log.d(TAG, "Webkit prevent current but not last event, enqueue ui event");
                /// Only one event is preventDefault, Ui also handle this touch event.
                enqueueUiEventUnbatchedLocked(d);
              }
              unscheduleLongPressLocked();
              /// @ }
            }
          } else {
            // Web kit is being friendly.  Pass the event to the UI.
            enqueueUiEventUnbatchedLocked(d);
          }
        }

        if (event != null && recycleEvent) {
          event.recycle();
        }

        if (eventType == EVENT_TYPE_CLICK) {
          scheduleHideTapHighlightLocked();
        }
      }
    }
  }

  // Runs on web kit thread.
  private boolean dispatchWebKitEvent(MotionEvent event, int eventType, int flags) {
    if (DEBUG) {
      Log.d(
          TAG,
          "dispatchWebKitEvent: event=" + event + ", eventType=" + eventType + ", flags=" + flags);
    }
    boolean preventDefault = mWebKitCallbacks.dispatchWebKitEvent(this, event, eventType, flags);
    if (DEBUG) {
      Log.d(TAG, "dispatchWebKitEvent: preventDefault=" + preventDefault);
    }
    return preventDefault;
  }

  private boolean isMoveEventLocked(DispatchEvent d) {
    return d.mEvent != null && d.mEvent.getActionMasked() == MotionEvent.ACTION_MOVE;
  }

  private void drainStaleWebKitEventsLocked() {
    DispatchEvent d = mWebKitDispatchEventQueue.mHead;
    while (d != null && d.mNext != null && isMoveEventLocked(d) && isMoveEventLocked(d.mNext)) {
      DispatchEvent next = d.mNext;
      skipWebKitEventLocked(d);
      d = next;
    }
    mWebKitDispatchEventQueue.mHead = d;
  }

  // Called by WebKit when it doesn't care about the rest of the touch stream
  public void skipWebkitForRemainingTouchStream() {
    // Just treat this like a timeout
    handleWebKitTimeout();
  }

  // Runs on UI thread in response to the web kit thread appearing to be unresponsive.
  private void handleWebKitTimeout() {
    synchronized (mLock) {
      if (!mWebKitTimeoutScheduled) {
        return;
      }
      mWebKitTimeoutScheduled = false;

      if (DEBUG) {
        Log.d(TAG, "handleWebKitTimeout: timeout occurred!");
      }

      // Drain the web kit event queue.
      DispatchEvent d = mWebKitDispatchEventQueue.dequeueList();

      // If web kit was processing an event (must be at the head of the list because
      // it can only do one at a time), then clone it or ignore it.
      if ((d.mFlags & FLAG_WEBKIT_IN_PROGRESS) != 0) {
        d.mFlags |= FLAG_WEBKIT_TIMEOUT;
        if ((d.mFlags & FLAG_PRIVATE) != 0) {
          d = d.mNext; // the event is private to web kit, ignore it
        } else {
          d = copyDispatchEventLocked(d);
          d.mFlags &= ~FLAG_WEBKIT_IN_PROGRESS;
        }
      }

      // Enqueue all non-private events for handling by the UI thread.
      while (d != null) {
        DispatchEvent next = d.mNext;
        skipWebKitEventLocked(d);
        d = next;
      }

      // Tell web kit to cancel all pending touches.
      // This also prevents us from sending web kit any more touches until the
      // next gesture begins.  (As required to ensure touch event stream consistency.)
      enqueueWebKitCancelTouchEventIfNeededLocked();
    }
  }

  private void skipWebKitEventLocked(DispatchEvent d) {
    d.mNext = null;
    if ((d.mFlags & FLAG_PRIVATE) != 0) {
      recycleDispatchEventLocked(d);
    } else {
      d.mFlags |= FLAG_WEBKIT_TIMEOUT;
      enqueueUiEventUnbatchedLocked(d);
    }
  }

  /**
   * Dispatches pending UI events. Must only be called from the UI thread.
   *
   * <p>This method may be used to flush the queue of pending input events immediately. This method
   * may help to reduce input dispatch latency if called before certain expensive operations such as
   * drawing.
   */
  public void dispatchUiEvents() {
    dispatchUiEvents(false);
  }

  private void dispatchUiEvents(boolean calledFromHandler) {
    for (; ; ) {
      MotionEvent event;
      final int eventType;
      final int flags;
      synchronized (mLock) {
        DispatchEvent d = mUiDispatchEventQueue.dequeue();
        if (d == null) {
          if (mUiDispatchScheduled) {
            mUiDispatchScheduled = false;
            if (!calledFromHandler) {
              mUiHandler.removeMessages(UiHandler.MSG_DISPATCH_UI_EVENTS);
            }
          }
          return;
        }

        event = d.mEvent;
        if (event != null && (d.mFlags & FLAG_WEBKIT_TRANSFORMED_EVENT) != 0) {
          event.scale(1.0f / d.mWebKitScale);
          event.offsetLocation(-d.mWebKitXOffset, -d.mWebKitYOffset);
          d.mFlags &= ~FLAG_WEBKIT_TRANSFORMED_EVENT;
        }

        eventType = d.mEventType;
        if (eventType == EVENT_TYPE_TOUCH) {
          event = mUiTouchStream.update(event);
          if (DEBUG && event == null && d.mEvent != null) {
            Log.d(TAG, "dispatchUiEvents: dropped event " + d.mEvent);
          }
        }

        flags = d.mFlags;

        if (event == d.mEvent) {
          d.mEvent = null; // retain ownership of event, don't recycle it yet
        }
        recycleDispatchEventLocked(d);

        if (eventType == EVENT_TYPE_CLICK) {
          scheduleHideTapHighlightLocked();
        }
      }

      // Handle the event.
      if (event != null) {
        dispatchUiEvent(event, eventType, flags);
        event.recycle();
      }
    }
  }

  // Runs on UI thread.
  private void dispatchUiEvent(MotionEvent event, int eventType, int flags) {
    if (DEBUG) {
      Log.d(
          TAG, "dispatchUiEvent: event=" + event + ", eventType=" + eventType + ", flags=" + flags);
    }
    mUiCallbacks.dispatchUiEvent(event, eventType, flags);
  }

  private void enqueueEventLocked(DispatchEvent d) {
    if (!shouldSkipWebKit(d)) {
      enqueueWebKitEventLocked(d);
    } else {
      enqueueUiEventLocked(d);
    }
  }

  private boolean shouldSkipWebKit(DispatchEvent d) {
    switch (d.mEventType) {
      case EVENT_TYPE_CLICK:
      case EVENT_TYPE_HOVER:
      case EVENT_TYPE_SCROLL:
      case EVENT_TYPE_HIT_TEST:
        return false;
      case EVENT_TYPE_TOUCH:
        // TODO: This should be cleaned up. We now have WebViewInputDispatcher
        // and WebViewClassic both checking for slop and doing their own
        // thing - they should be consolidated. And by consolidated, I mean
        // WebViewClassic's version should just be deleted.
        // The reason this is done is because webpages seem to expect
        // that they only get an ontouchmove if the slop has been exceeded.
        if (mIsTapCandidate
            && d.mEvent != null
            && d.mEvent.getActionMasked() == MotionEvent.ACTION_MOVE) {
          return true;
        }
        return !mPostSendTouchEventsToWebKit || mPostDoNotSendTouchEventsToWebKitUntilNextGesture;
    }
    return true;
  }

  private void enqueueWebKitCancelTouchEventIfNeededLocked() {
    // We want to cancel touch events that were delivered to web kit.
    // Enqueue a null event at the end of the queue if needed.
    if (mWebKitTouchStream.isCancelNeeded() || !mWebKitDispatchEventQueue.isEmpty()) {
      DispatchEvent d = obtainDispatchEventLocked(null, EVENT_TYPE_TOUCH, FLAG_PRIVATE, 0, 0, 1.0f);
      enqueueWebKitEventUnbatchedLocked(d);
      mPostDoNotSendTouchEventsToWebKitUntilNextGesture = true;
    }
  }

  private void enqueueWebKitEventLocked(DispatchEvent d) {
    if (batchEventLocked(d, mWebKitDispatchEventQueue.mTail)) {
      if (DEBUG) {
        Log.d(TAG, "enqueueWebKitEventLocked: batched event " + d.mEvent);
      }
      recycleDispatchEventLocked(d);
    } else {
      enqueueWebKitEventUnbatchedLocked(d);
    }
  }

  private void enqueueWebKitEventUnbatchedLocked(DispatchEvent d) {
    if (DEBUG) {
      Log.d(TAG, "enqueueWebKitEventUnbatchedLocked: enqueued event " + d.mEvent);
    }
    mWebKitDispatchEventQueue.enqueue(d);
    scheduleWebKitDispatchLocked();
    updateWebKitTimeoutLocked();
  }

  private void scheduleWebKitDispatchLocked() {
    if (!mWebKitDispatchScheduled) {
      mWebKitHandler.sendEmptyMessage(WebKitHandler.MSG_DISPATCH_WEBKIT_EVENTS);
      mWebKitDispatchScheduled = true;
    }
  }

  private void updateWebKitTimeoutLocked() {
    DispatchEvent d = mWebKitDispatchEventQueue.mHead;
    if (d != null && mWebKitTimeoutScheduled && mWebKitTimeoutTime == d.mTimeoutTime) {
      return;
    }
    if (mWebKitTimeoutScheduled) {
      mUiHandler.removeMessages(UiHandler.MSG_WEBKIT_TIMEOUT);
      mWebKitTimeoutScheduled = false;
    }
    if (d != null) {
      mUiHandler.sendEmptyMessageAtTime(UiHandler.MSG_WEBKIT_TIMEOUT, d.mTimeoutTime);
      mWebKitTimeoutScheduled = true;
      mWebKitTimeoutTime = d.mTimeoutTime;
    }
  }

  private void enqueueUiCancelTouchEventIfNeededLocked() {
    // We want to cancel touch events that were delivered to the UI.
    // Enqueue a null event at the end of the queue if needed.
    if (mUiTouchStream.isCancelNeeded() || !mUiDispatchEventQueue.isEmpty()) {
      DispatchEvent d = obtainDispatchEventLocked(null, EVENT_TYPE_TOUCH, FLAG_PRIVATE, 0, 0, 1.0f);
      enqueueUiEventUnbatchedLocked(d);
    }
  }

  private void enqueueUiEventLocked(DispatchEvent d) {
    if (batchEventLocked(d, mUiDispatchEventQueue.mTail)) {
      if (DEBUG) {
        Log.d(TAG, "enqueueUiEventLocked: batched event " + d.mEvent);
      }
      recycleDispatchEventLocked(d);
    } else {
      enqueueUiEventUnbatchedLocked(d);
    }
  }

  private void enqueueUiEventUnbatchedLocked(DispatchEvent d) {
    if (DEBUG) {
      Log.d(TAG, "enqueueUiEventUnbatchedLocked: enqueued event " + d.mEvent);
    }
    mUiDispatchEventQueue.enqueue(d);
    scheduleUiDispatchLocked();
  }

  private void scheduleUiDispatchLocked() {
    if (!mUiDispatchScheduled) {
      mUiHandler.sendEmptyMessage(UiHandler.MSG_DISPATCH_UI_EVENTS);
      mUiDispatchScheduled = true;
    }
  }

  private boolean batchEventLocked(DispatchEvent in, DispatchEvent tail) {
    if (!ENABLE_EVENT_BATCHING) {
      return false;
    }
    if (tail != null
        && tail.mEvent != null
        && in.mEvent != null
        && in.mEventType == tail.mEventType
        && in.mFlags == tail.mFlags
        && in.mWebKitXOffset == tail.mWebKitXOffset
        && in.mWebKitYOffset == tail.mWebKitYOffset
        && in.mWebKitScale == tail.mWebKitScale) {
      return tail.mEvent.addBatch(in.mEvent);
    }
    return false;
  }

  private DispatchEvent obtainDispatchEventLocked(
      MotionEvent event,
      int eventType,
      int flags,
      int webKitXOffset,
      int webKitYOffset,
      float webKitScale) {
    DispatchEvent d = obtainUninitializedDispatchEventLocked();
    d.mEvent = event;
    d.mEventType = eventType;
    d.mFlags = flags;
    d.mTimeoutTime = SystemClock.uptimeMillis() + WEBKIT_TIMEOUT_MILLIS;
    d.mWebKitXOffset = webKitXOffset;
    d.mWebKitYOffset = webKitYOffset;
    d.mWebKitScale = webKitScale;
    if (DEBUG) {
      Log.d(TAG, "Timeout time: " + (d.mTimeoutTime - SystemClock.uptimeMillis()));
    }
    return d;
  }

  private DispatchEvent copyDispatchEventLocked(DispatchEvent d) {
    DispatchEvent copy = obtainUninitializedDispatchEventLocked();
    if (d.mEvent != null) {
      copy.mEvent = d.mEvent.copy();
    }
    copy.mEventType = d.mEventType;
    copy.mFlags = d.mFlags;
    copy.mTimeoutTime = d.mTimeoutTime;
    copy.mWebKitXOffset = d.mWebKitXOffset;
    copy.mWebKitYOffset = d.mWebKitYOffset;
    copy.mWebKitScale = d.mWebKitScale;
    copy.mNext = d.mNext;
    return copy;
  }

  private DispatchEvent obtainUninitializedDispatchEventLocked() {
    DispatchEvent d = mDispatchEventPool;
    if (d != null) {
      mDispatchEventPoolSize -= 1;
      mDispatchEventPool = d.mNext;
      d.mNext = null;
    } else {
      d = new DispatchEvent();
    }
    return d;
  }

  private void recycleDispatchEventLocked(DispatchEvent d) {
    if (d.mEvent != null) {
      d.mEvent.recycle();
      d.mEvent = null;
    }

    if (mDispatchEventPoolSize < MAX_DISPATCH_EVENT_POOL_SIZE) {
      mDispatchEventPoolSize += 1;
      d.mNext = mDispatchEventPool;
      mDispatchEventPool = d;
    }
  }

  /* Implemented by {@link WebViewClassic} to perform operations on the UI thread. */
  public static interface UiCallbacks {
    /**
     * Gets the UI thread's looper.
     *
     * @return The looper.
     */
    public Looper getUiLooper();

    /**
     * Gets the UI's context
     *
     * @return The context
     */
    public Context getContext();

    /**
     * Dispatches an event to the UI.
     *
     * @param event The event.
     * @param eventType The event type.
     * @param flags The event's dispatch flags.
     */
    public void dispatchUiEvent(MotionEvent event, int eventType, int flags);

    /**
     * Asks the UI thread whether this touch event stream should be intercepted based on the touch
     * down event.
     *
     * @param event The touch down event.
     * @return true if the UI stream wants the touch stream without going through webkit or false
     *     otherwise.
     */
    public boolean shouldInterceptTouchEvent(MotionEvent event);

    /**
     * Inform's the UI that it should show the tap highlight
     *
     * @param show True if it should show the highlight, false if it should hide it
     */
    public void showTapHighlight(boolean show);

    /**
     * Called when we are sending a new EVENT_TYPE_HIT_TEST to WebKit, so previous hit tests should
     * be cleared as they are obsolete.
     */
    public void clearPreviousHitTest();
  }

  /* Implemented by {@link WebViewCore} to perform operations on the web kit thread. */
  public static interface WebKitCallbacks {
    /**
     * Gets the web kit thread's looper.
     *
     * @return The looper.
     */
    public Looper getWebKitLooper();

    /**
     * Dispatches an event to web kit.
     *
     * @param dispatcher The WebViewInputDispatcher sending the event
     * @param event The event.
     * @param eventType The event type.
     * @param flags The event's dispatch flags.
     * @return True if web kit wants to prevent default event handling.
     */
    public boolean dispatchWebKitEvent(
        WebViewInputDispatcher dispatcher, MotionEvent event, int eventType, int flags);
  }

  // Runs on UI thread.
  private final class UiHandler extends Handler {
    public static final int MSG_DISPATCH_UI_EVENTS = 1;
    public static final int MSG_WEBKIT_TIMEOUT = 2;
    public static final int MSG_LONG_PRESS = 3;
    public static final int MSG_CLICK = 4;
    public static final int MSG_SHOW_TAP_HIGHLIGHT = 5;
    public static final int MSG_HIDE_TAP_HIGHLIGHT = 6;

    public UiHandler(Looper looper) {
      super(looper);
    }

    @Override
    public void handleMessage(Message msg) {
      switch (msg.what) {
        case MSG_DISPATCH_UI_EVENTS:
          dispatchUiEvents(true);
          break;
        case MSG_WEBKIT_TIMEOUT:
          handleWebKitTimeout();
          break;
        case MSG_LONG_PRESS:
          postLongPress();
          break;
        case MSG_CLICK:
          postClick();
          break;
        case MSG_SHOW_TAP_HIGHLIGHT:
          postShowTapHighlight(true);
          break;
        case MSG_HIDE_TAP_HIGHLIGHT:
          postShowTapHighlight(false);
          break;
        default:
          throw new IllegalStateException("Unknown message type: " + msg.what);
      }
    }
  }

  // Runs on web kit thread.
  private final class WebKitHandler extends Handler {
    public static final int MSG_DISPATCH_WEBKIT_EVENTS = 1;

    public WebKitHandler(Looper looper) {
      super(looper);
    }

    @Override
    public void handleMessage(Message msg) {
      switch (msg.what) {
        case MSG_DISPATCH_WEBKIT_EVENTS:
          dispatchWebKitEvents(true);
          break;
        default:
          throw new IllegalStateException("Unknown message type: " + msg.what);
      }
    }
  }

  private static final class DispatchEvent {
    public DispatchEvent mNext;

    public MotionEvent mEvent;
    public int mEventType;
    public int mFlags;
    public long mTimeoutTime;
    public int mWebKitXOffset;
    public int mWebKitYOffset;
    public float mWebKitScale;
  }

  private static final class DispatchEventQueue {
    public DispatchEvent mHead;
    public DispatchEvent mTail;

    public boolean isEmpty() {
      return mHead != null;
    }

    public void enqueue(DispatchEvent d) {
      if (mHead == null) {
        mHead = d;
        mTail = d;
      } else {
        mTail.mNext = d;
        mTail = d;
      }
    }

    public DispatchEvent dequeue() {
      DispatchEvent d = mHead;
      if (d != null) {
        DispatchEvent next = d.mNext;
        if (next == null) {
          mHead = null;
          mTail = null;
        } else {
          mHead = next;
          d.mNext = null;
        }
      }
      return d;
    }

    public DispatchEvent dequeueList() {
      DispatchEvent d = mHead;
      if (d != null) {
        mHead = null;
        mTail = null;
      }
      return d;
    }
  }

  /**
   * Keeps track of a stream of touch events so that we can discard touch events that would make the
   * stream inconsistent.
   */
  private static final class TouchStream {
    private MotionEvent mLastEvent;

    /**
     * Gets the last touch event that was delivered.
     *
     * @return The last touch event, or null if none.
     */
    public MotionEvent getLastEvent() {
      return mLastEvent;
    }

    /**
     * Updates the touch event stream.
     *
     * @param event The event that we intend to send, or null to cancel the touch event stream.
     * @return The event that we should actually send, or null if no event should be sent because
     *     the proposed event would make the stream inconsistent.
     */
    public MotionEvent update(MotionEvent event) {
      if (event == null) {
        if (isCancelNeeded()) {
          event = mLastEvent;
          if (event != null) {
            event.setAction(MotionEvent.ACTION_CANCEL);
            mLastEvent = null;
          }
        }
        return event;
      }

      switch (event.getActionMasked()) {
        case MotionEvent.ACTION_MOVE:
        case MotionEvent.ACTION_UP:
        case MotionEvent.ACTION_POINTER_DOWN:
        case MotionEvent.ACTION_POINTER_UP:
          if (mLastEvent == null || mLastEvent.getAction() == MotionEvent.ACTION_UP) {
            return null;
          }
          updateLastEvent(event);
          return event;

        case MotionEvent.ACTION_DOWN:
          updateLastEvent(event);
          return event;

        case MotionEvent.ACTION_CANCEL:
          if (mLastEvent == null) {
            return null;
          }
          updateLastEvent(null);
          return event;

        default:
          return null;
      }
    }

    /**
     * Returns true if there is a gesture in progress that may need to be canceled.
     *
     * @return True if cancel is needed.
     */
    public boolean isCancelNeeded() {
      return mLastEvent != null && mLastEvent.getAction() != MotionEvent.ACTION_UP;
    }

    private void updateLastEvent(MotionEvent event) {
      if (mLastEvent != null) {
        mLastEvent.recycle();
      }
      mLastEvent = event != null ? MotionEvent.obtainNoHistory(event) : null;
    }
  }

  /**
   * M: Keeps track of a stream of touch events so that we can prevent Ui to handle events that
   * previous event is also prevent.
   */
  private static final class PreventTouchStream {
    private boolean mLastPreventDefault;

    public boolean getLastPreventDefault() {
      return mLastPreventDefault;
    }
    /**
     * Update the prevent touch stream.
     *
     * @param event The event that webkit had handled, or null to drop it.
     * @param preventDefault Prevent Ui to handle this event or not.
     * @return true If last event if prevented or false if not..
     */
    public boolean update(MotionEvent event, boolean preventDefault) {
      // Don't prevent null event.
      if (event == null) {
        Log.e(TAG, "PreventTouchStream has null event.");
        return false;
      }

      if (DEBUG) {
        Log.d(TAG, "event action=" + event.getActionMasked() + " preventDefault=" + preventDefault);
      }

      boolean prevent = mLastPreventDefault;
      mLastPreventDefault = preventDefault;
      switch (event.getActionMasked()) {
        case MotionEvent.ACTION_DOWN:
        case MotionEvent.ACTION_MOVE:
        case MotionEvent.ACTION_POINTER_DOWN:
        case MotionEvent.ACTION_POINTER_UP:
          return prevent;
        case MotionEvent.ACTION_UP:
        case MotionEvent.ACTION_CANCEL:
          mLastPreventDefault = false;
          return prevent;
        default:
          return preventDefault;
      }
    }
  }
}
class init
    implements init
{

    private static final int DOUBLE_TAP_TIMEOUT = ViewConfiguration.getDoubleTapTimeout();
    private static final int LONGPRESS_TIMEOUT = ViewConfiguration.getLongPressTimeout();
    private static final int LONG_PRESS = 2;
    private static final int SHOW_PRESS = 1;
    private static final int TAP = 3;
    private static final int TAP_TIMEOUT = ViewConfiguration.getTapTimeout();
    private boolean mAlwaysInBiggerTapRegion;
    private boolean mAlwaysInTapRegion;
    private MotionEvent mCurrentDownEvent;
    private boolean mDeferConfirmSingleTap;
    private android.view.atImplBase.mDoubleTapListener mDoubleTapListener;
    private int mDoubleTapSlopSquare;
    private float mDownFocusX;
    private float mDownFocusY;
    private final Handler mHandler;
    private boolean mInLongPress;
    private boolean mIsDoubleTapping;
    private boolean mIsLongpressEnabled;
    private float mLastFocusX;
    private float mLastFocusY;
    private final android.view.atImplBase.mDoubleTapListener mListener;
    private int mMaximumFlingVelocity;
    private int mMinimumFlingVelocity;
    private MotionEvent mPreviousUpEvent;
    private boolean mStillDown;
    private int mTouchSlopSquare;
    private VelocityTracker mVelocityTracker;

    private void cancel()
    {
        mHandler.removeMessages(1);
        mHandler.removeMessages(2);
        mHandler.removeMessages(3);
        mVelocityTracker.recycle();
        mVelocityTracker = null;
        mIsDoubleTapping = false;
        mStillDown = false;
        mAlwaysInTapRegion = false;
        mAlwaysInBiggerTapRegion = false;
        mDeferConfirmSingleTap = false;
        if (mInLongPress)
        {
            mInLongPress = false;
        }
    }

    private void cancelTaps()
    {
        mHandler.removeMessages(1);
        mHandler.removeMessages(2);
        mHandler.removeMessages(3);
        mIsDoubleTapping = false;
        mAlwaysInTapRegion = false;
        mAlwaysInBiggerTapRegion = false;
        mDeferConfirmSingleTap = false;
        if (mInLongPress)
        {
            mInLongPress = false;
        }
    }

    private void dispatchLongPress()
    {
        mHandler.removeMessages(3);
        mDeferConfirmSingleTap = false;
        mInLongPress = true;
        mListener.mListener(mCurrentDownEvent);
    }

    private void init(Context context)
    {
        if (context == null)
        {
            throw new IllegalArgumentException("Context must not be null");
        }
        if (mListener == null)
        {
            throw new IllegalArgumentException("OnGestureListener must not be null");
        } else
        {
            mIsLongpressEnabled = true;
            context = ViewConfiguration.get(context);
            int i = context.getScaledTouchSlop();
            int j = context.getScaledDoubleTapSlop();
            mMinimumFlingVelocity = context.getScaledMinimumFlingVelocity();
            mMaximumFlingVelocity = context.getScaledMaximumFlingVelocity();
            mTouchSlopSquare = i * i;
            mDoubleTapSlopSquare = j * j;
            return;
        }
    }

    private boolean isConsideredDoubleTap(MotionEvent motionevent, MotionEvent motionevent1, MotionEvent motionevent2)
    {
        if (mAlwaysInBiggerTapRegion && motionevent2.getEventTime() - motionevent1.getEventTime() <= (long)DOUBLE_TAP_TIMEOUT)
        {
            int i = (int)motionevent.getX() - (int)motionevent2.getX();
            int j = (int)motionevent.getY() - (int)motionevent2.getY();
            if (i * i + j * j < mDoubleTapSlopSquare)
            {
                return true;
            }
        }
        return false;
    }

    public boolean isLongpressEnabled()
    {
        return mIsLongpressEnabled;
    }

    public boolean onTouchEvent(MotionEvent motionevent)
    {
        float f;
        float f1;
        int i2;
        int j2 = motionevent.getAction();
        if (mVelocityTracker == null)
        {
            mVelocityTracker = VelocityTracker.obtain();
        }
        mVelocityTracker.addMovement(motionevent);
        boolean flag;
        int l;
        int k1;
        if ((j2 & 0xff) == 6)
        {
            flag = true;
        } else
        {
            flag = false;
        }
        if (flag)
        {
            l = MotionEventCompat.getActionIndex(motionevent);
        } else
        {
            l = -1;
        }
        i2 = MotionEventCompat.getPointerCount(motionevent);
        k1 = 0;
        f = 0.0F;
        float f2;
        for (f1 = 0.0F; k1 < i2; f1 = f2)
        {
            float f5 = f;
            f2 = f1;
            if (l != k1)
            {
                f2 = f1 + MotionEventCompat.getX(motionevent, k1);
                f5 = f + MotionEventCompat.getY(motionevent, k1);
            }
            k1++;
            f = f5;
        }

        int i;
        if (flag)
        {
            i = i2 - 1;
        } else
        {
            i = i2;
        }
        f1 /= i;
        f /= i;
        j2 & 0xff;
        JVM INSTR tableswitch 0 6: default 204
    //                   0 383
    //                   1 862
    //                   2 643
    //                   3 1136
    //                   4 204
    //                   5 213
    //                   6 239;
           goto _L1 _L2 _L3 _L4 _L5 _L1 _L6 _L7
_L1:
        return false;
_L6:
        mLastFocusX = f1;
        mDownFocusX = f1;
        mLastFocusY = f;
        mDownFocusY = f;
        cancelTaps();
        return false;
_L7:
        mLastFocusX = f1;
        mDownFocusX = f1;
        mLastFocusY = f;
        mDownFocusY = f;
        mVelocityTracker.computeCurrentVelocity(1000, mMaximumFlingVelocity);
        int i1 = MotionEventCompat.getActionIndex(motionevent);
        int j = MotionEventCompat.getPointerId(motionevent, i1);
        f = VelocityTrackerCompat.getXVelocity(mVelocityTracker, j);
        f1 = VelocityTrackerCompat.getYVelocity(mVelocityTracker, j);
        j = 0;
        while (j < i2) 
        {
            if (j != i1)
            {
                int l1 = MotionEventCompat.getPointerId(motionevent, j);
                float f3 = VelocityTrackerCompat.getXVelocity(mVelocityTracker, l1);
                if (VelocityTrackerCompat.getYVelocity(mVelocityTracker, l1) * f1 + f3 * f < 0.0F)
                {
                    mVelocityTracker.clear();
                    return false;
                }
            }
            j++;
        }
          goto _L8
_L2:
        if (mDoubleTapListener == null) goto _L10; else goto _L9
_L9:
        boolean flag2;
        flag2 = mHandler.hasMessages(3);
        if (flag2)
        {
            mHandler.removeMessages(3);
        }
        if (mCurrentDownEvent == null || mPreviousUpEvent == null || !flag2 || !isConsideredDoubleTap(mCurrentDownEvent, mPreviousUpEvent, motionevent)) goto _L12; else goto _L11
_L11:
        boolean flag1;
        mIsDoubleTapping = true;
        flag1 = mDoubleTapListener.mDoubleTapListener(mCurrentDownEvent) | false | mDoubleTapListener.(motionevent);
_L13:
        mLastFocusX = f1;
        mDownFocusX = f1;
        mLastFocusY = f;
        mDownFocusY = f;
        if (mCurrentDownEvent != null)
        {
            mCurrentDownEvent.recycle();
        }
        mCurrentDownEvent = MotionEvent.obtain(motionevent);
        mAlwaysInTapRegion = true;
        mAlwaysInBiggerTapRegion = true;
        mStillDown = true;
        mInLongPress = false;
        mDeferConfirmSingleTap = false;
        if (mIsLongpressEnabled)
        {
            mHandler.removeMessages(2);
            mHandler.sendEmptyMessageAtTime(2, mCurrentDownEvent.getDownTime() + (long)TAP_TIMEOUT + (long)LONGPRESS_TIMEOUT);
        }
        mHandler.sendEmptyMessageAtTime(1, mCurrentDownEvent.getDownTime() + (long)TAP_TIMEOUT);
        return flag1 | mListener.mListener(motionevent);
_L12:
        mHandler.sendEmptyMessageDelayed(3, DOUBLE_TAP_TIMEOUT);
_L10:
        flag1 = false;
        if (true) goto _L13; else goto _L4
_L4:
        if (!mInLongPress)
        {
            float f4 = mLastFocusX - f1;
            float f6 = mLastFocusY - f;
            if (mIsDoubleTapping)
            {
                return mDoubleTapListener.(motionevent) | false;
            }
            if (mAlwaysInTapRegion)
            {
                int k = (int)(f1 - mDownFocusX);
                int j1 = (int)(f - mDownFocusY);
                k = k * k + j1 * j1;
                MotionEvent motionevent1;
                VelocityTracker velocitytracker;
                boolean flag3;
                boolean flag4;
                if (k > mTouchSlopSquare)
                {
                    flag3 = mListener.mListener(mCurrentDownEvent, motionevent, f4, f6);
                    mLastFocusX = f1;
                    mLastFocusY = f;
                    mAlwaysInTapRegion = false;
                    mHandler.removeMessages(3);
                    mHandler.removeMessages(1);
                    mHandler.removeMessages(2);
                } else
                {
                    flag3 = false;
                }
                if (k > mTouchSlopSquare)
                {
                    mAlwaysInBiggerTapRegion = false;
                }
                return flag3;
            }
            if (Math.abs(f4) >= 1.0F || Math.abs(f6) >= 1.0F)
            {
                flag3 = mListener.mListener(mCurrentDownEvent, motionevent, f4, f6);
                mLastFocusX = f1;
                mLastFocusY = f;
                return flag3;
            }
        }
_L8:
        if (true) goto _L1; else goto _L3
_L3:
        mStillDown = false;
        motionevent1 = MotionEvent.obtain(motionevent);
        if (mIsDoubleTapping)
        {
            flag3 = mDoubleTapListener.(motionevent) | false;
        } else
        if (mInLongPress)
        {
            mHandler.removeMessages(3);
            mInLongPress = false;
            flag3 = false;
        } else
        if (mAlwaysInTapRegion)
        {
            flag4 = mListener.mListener(motionevent);
            flag3 = flag4;
            if (mDeferConfirmSingleTap)
            {
                flag3 = flag4;
                if (mDoubleTapListener != null)
                {
                    mDoubleTapListener.rmed(motionevent);
                    flag3 = flag4;
                }
            }
        } else
        {
            velocitytracker = mVelocityTracker;
            k = MotionEventCompat.getPointerId(motionevent, 0);
            velocitytracker.computeCurrentVelocity(1000, mMaximumFlingVelocity);
            f = VelocityTrackerCompat.getYVelocity(velocitytracker, k);
            f1 = VelocityTrackerCompat.getXVelocity(velocitytracker, k);
            if (Math.abs(f) > (float)mMinimumFlingVelocity || Math.abs(f1) > (float)mMinimumFlingVelocity)
            {
                flag3 = mListener.mListener(mCurrentDownEvent, motionevent, f1, f);
            } else
            {
                flag3 = false;
            }
        }
        if (mPreviousUpEvent != null)
        {
            mPreviousUpEvent.recycle();
        }
        mPreviousUpEvent = motionevent1;
        if (mVelocityTracker != null)
        {
            mVelocityTracker.recycle();
            mVelocityTracker = null;
        }
        mIsDoubleTapping = false;
        mDeferConfirmSingleTap = false;
        mHandler.removeMessages(1);
        mHandler.removeMessages(2);
        return flag3;
_L5:
        cancel();
        return false;
    }
 @Override
 public boolean onTouchEvent(MotionEvent ev) {
     int action = ev.getAction();
     boolean handled = false;
     switch (action) {
         case MotionEvent.ACTION_DOWN: {
             View v = getCurrentView();
             if (v != null) {
                 if (isTransformedTouchPointInView(ev.getX(), ev.getY(), v, null)) {
                     if (mPendingCheckForTap == null) {
                         mPendingCheckForTap = new CheckForTap();
                     }
                     mTouchMode = TOUCH_MODE_DOWN_IN_CURRENT_VIEW;
                     postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
                 }
             }
             break;
         }
         case MotionEvent.ACTION_MOVE: break;
         case MotionEvent.ACTION_POINTER_UP: break;
         case MotionEvent.ACTION_UP: {
             if (mTouchMode == TOUCH_MODE_DOWN_IN_CURRENT_VIEW) {
                 final View v = getCurrentView();
                 final ViewAndMetaData viewData = getMetaDataForChild(v);
                 if (v != null) {
                     if (isTransformedTouchPointInView(ev.getX(), ev.getY(), v, null)) {
                         final Handler handler = getHandler();
                         if (handler != null) {
                             handler.removeCallbacks(mPendingCheckForTap);
                         }
                         showTapFeedback(v);
                         postDelayed(new Runnable() {
                             public void run() {
                                 hideTapFeedback(v);
                                 post(new Runnable() {
                                     public void run() {
                                         if (viewData != null) {
                                             performItemClick(v, viewData.adapterPosition,
                                                     viewData.itemId);
                                         } else {
                                             performItemClick(v, 0, 0);
                                         }
                                     }
                                 });
                             }
                         }, ViewConfiguration.getPressedStateDuration());
                         handled = true;
                     }
                 }
             }
             mTouchMode = TOUCH_MODE_NONE;
             break;
         }
         case MotionEvent.ACTION_CANCEL: {
             View v = getCurrentView();
             if (v != null) {
                 hideTapFeedback(v);
             }
             mTouchMode = TOUCH_MODE_NONE;
         }
     }
     return handled;
 }
  @Override
  public boolean onTouchEvent(MotionEvent ev) {
    final int action = ev.getAction();
    boolean wasHeaderChildBeingPressed = mHeaderChildBeingPressed;
    if (mHeaderChildBeingPressed) {
      final View tempHeader = getHeaderAt(mMotionHeaderPosition);
      final View headerHolder =
          mMotionHeaderPosition == MATCHED_STICKIED_HEADER
              ? tempHeader
              : getChildAt(mMotionHeaderPosition);
      if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
        mHeaderChildBeingPressed = false;
      }
      if (tempHeader != null) {
        tempHeader.dispatchTouchEvent(transformEvent(ev, mMotionHeaderPosition));
        tempHeader.invalidate();
        tempHeader.postDelayed(
            new Runnable() {
              public void run() {
                invalidate(
                    0,
                    headerHolder.getTop(),
                    getWidth(),
                    headerHolder.getTop() + headerHolder.getHeight());
              }
            },
            ViewConfiguration.getPressedStateDuration());
        invalidate(
            0, headerHolder.getTop(), getWidth(), headerHolder.getTop() + headerHolder.getHeight());
      }
    }

    switch (action & MotionEvent.ACTION_MASK) {
      case MotionEvent.ACTION_DOWN:
        if (mPendingCheckForTap == null) {
          mPendingCheckForTap = new CheckForHeaderTap();
        }
        postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());

        final int y = (int) ev.getY();
        mMotionY = y;
        mMotionHeaderPosition = findMotionHeader(y);
        if (mMotionHeaderPosition == NO_MATCHED_HEADER || mScrollState == SCROLL_STATE_FLING) {
          // Don't consume the event and pass it to super because we
          // can't handle it yet.
          break;
        } else {
          View tempHeader = getHeaderAt(mMotionHeaderPosition);
          if (tempHeader != null) {
            if (tempHeader.dispatchTouchEvent(transformEvent(ev, mMotionHeaderPosition))) {
              mHeaderChildBeingPressed = true;
              tempHeader.setPressed(true);
            }
            tempHeader.invalidate();
            if (mMotionHeaderPosition != MATCHED_STICKIED_HEADER) {
              tempHeader = getChildAt(mMotionHeaderPosition);
            }
            invalidate(
                0, tempHeader.getTop(), getWidth(), tempHeader.getTop() + tempHeader.getHeight());
          }
        }
        mTouchMode = TOUCH_MODE_DOWN;
        return true;
      case MotionEvent.ACTION_MOVE:
        if (mMotionHeaderPosition != NO_MATCHED_HEADER
            && Math.abs(ev.getY() - mMotionY) > mTouchSlop) {
          // Detected scroll initiation so cancel touch completion on
          // header.
          mTouchMode = TOUCH_MODE_REST;
          // if (!mHeaderChildBeingPressed) {
          final View header = getHeaderAt(mMotionHeaderPosition);
          if (header != null) {
            header.setPressed(false);
            header.invalidate();
          }
          final Handler handler = getHandler();
          if (handler != null) {
            handler.removeCallbacks(mPendingCheckForLongPress);
          }
          mMotionHeaderPosition = NO_MATCHED_HEADER;
          // }
        }
        break;
      case MotionEvent.ACTION_UP:
        if (mTouchMode == TOUCH_MODE_FINISHED_LONG_PRESS) {
          mTouchMode = TOUCH_MODE_REST;
          return true;
        }
        if (mTouchMode == TOUCH_MODE_REST || mMotionHeaderPosition == NO_MATCHED_HEADER) {
          break;
        }

        final View header = getHeaderAt(mMotionHeaderPosition);
        if (!wasHeaderChildBeingPressed) {
          if (header != null) {
            if (mTouchMode != TOUCH_MODE_DOWN) {
              header.setPressed(false);
            }

            if (mPerformHeaderClick == null) {
              mPerformHeaderClick = new PerformHeaderClick();
            }

            final PerformHeaderClick performHeaderClick = mPerformHeaderClick;
            performHeaderClick.mClickMotionPosition = mMotionHeaderPosition;
            performHeaderClick.rememberWindowAttachCount();

            if (mTouchMode == TOUCH_MODE_DOWN || mTouchMode == TOUCH_MODE_TAP) {
              final Handler handler = getHandler();
              if (handler != null) {
                handler.removeCallbacks(
                    mTouchMode == TOUCH_MODE_DOWN
                        ? mPendingCheckForTap
                        : mPendingCheckForLongPress);
              }

              if (!mDataChanged) {
                /*
                 * Got here so must be a tap. The long press
                 * would have triggered on the callback handler.
                 */
                mTouchMode = TOUCH_MODE_TAP;
                header.setPressed(true);
                setPressed(true);
                if (mTouchModeReset != null) {
                  removeCallbacks(mTouchModeReset);
                }
                mTouchModeReset =
                    new Runnable() {
                      @Override
                      public void run() {
                        mMotionHeaderPosition = NO_MATCHED_HEADER;
                        mTouchModeReset = null;
                        mTouchMode = TOUCH_MODE_REST;
                        header.setPressed(false);
                        setPressed(false);
                        header.invalidate();
                        invalidate(0, header.getTop(), getWidth(), header.getHeight());
                        if (!mDataChanged) {
                          performHeaderClick.run();
                        }
                      }
                    };
                postDelayed(mTouchModeReset, ViewConfiguration.getPressedStateDuration());
              } else {
                mTouchMode = TOUCH_MODE_REST;
              }
            } else if (!mDataChanged) {
              performHeaderClick.run();
            }
          }
        }
        mTouchMode = TOUCH_MODE_REST;
        return true;
    }
    return super.onTouchEvent(ev);
  }
Beispiel #16
0
 private void startDelayTimer() {
   this.mPressDelayHandler.removeCallbacks(this.mPressDelayRunnable);
   this.mPressDelayHandler.postDelayed(
       this.mPressDelayRunnable, ViewConfiguration.getTapTimeout());
 }
/**
 * 1. Pop alphabet wave. 2. Show initial alphabet toast.
 *
 * @author wangziming
 */
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
public class AlphabetWavesView extends LinearLayout {

  /** Duration of fade-out animation. */
  private static final int DURATION_FADE_OUT = 300;

  /** Duration of fade-in animation. */
  private static final int DURATION_FADE_IN = 150;

  /** Duration of transition cross-fade animation. */
  private static final int DURATION_CROSS_FADE = 50;

  /** Inactivity timeout before fading controls. */
  private static final long FADE_TIMEOUT = 1500;

  /** Scroll thumb and preview not showing. */
  private static final int STATE_NONE = 0;

  /** Scroll thumb visible and moving along with the scrollbar. */
  private static final int STATE_VISIBLE = 1;

  /** Scroll thumb and preview being dragged by user. */
  private static final int STATE_CHANGE_TEXT = 2;

  private static final int POP_LIMIT_NUM = 10;

  private List<Alphabet> mAlphabetList;
  private HashMap<Integer, ValueAnimator> mAnimMap;
  private OnAlphabetListener mAlphabetListener;
  private float mSideIndexX;
  private float mSideIndexY;
  private int mSideIndexHeight;
  private int mIndexListSize;
  private int mViewWidth;
  private int mToastOffset;
  private int mToastTextSize;
  private int mAlphabetTextSize;
  private int mPaddingTopBottom;
  private int mAlphabetMaxOffset;
  private int mAlphabetLeftMargin;

  private int mMaxOffset;
  private int mMoveCount;
  private long mPopAnimTime;
  private long mBackAnimTime = 0;

  private float mLastFocusX;
  private float mLastFocusY;

  private Handler mHandler;
  private boolean mInSelect;
  private final int LONGPRESS_TIMEOUT = ViewConfiguration.getLongPressTimeout();
  private final int TAP_TIMEOUT = ViewConfiguration.getTapTimeout();
  private final int LONG_SELECT = 0;

  private Drawable mSelectedBg;
  private Drawable mToastBg;
  /** Defines the selectedBg's location and dimension at drawing time */
  private Rect mSelectedRect = new Rect();

  private ViewGroupOverlay mOverlay;
  private final TextView mPrimaryText;
  private final TextView mSecondaryText;
  private final ImageView mPreviewImage;

  private final Rect mContainerRect = new Rect();
  private final Rect mTempBounds = new Rect();
  private final Rect mTempMargins = new Rect();
  private final Rect mTempRect = new Rect();

  /** Set containing preview text transition animations. */
  private AnimatorSet mPreviewAnimation;
  /** Set containing decoration transition animations. */
  private AnimatorSet mDecorAnimation;

  /** Whether this view is currently performing layout. */
  private boolean mUpdatingLayout;

  /** Whether the primary text is showing. */
  private boolean mShowingPrimary;

  /** Whether the preview image is visible. */
  private boolean mShowingPreview;

  private int mPreSelection = -1;
  /** The index of the current section. */
  private int mCurrentSelection = -1;
  /**
   * Padding in pixels around the preview text. Applied as layout margins to the preview text and
   * padding to the preview image.
   */
  // private final int mPreviewPadding;

  private boolean isShowSelected;

  private boolean isPoped = false;

  boolean isSetList = false;

  /** Whether clip Alphabet, show initial. */
  private boolean mIsClipInitial = true;

  public AlphabetWavesView(Context context) {
    this(context, null);
  }

  public AlphabetWavesView(Context context, AttributeSet attrs) {
    this(context, attrs, R.attr.leAlphabetWavesViewStyle);
  }

  @SuppressLint("UseSparseArrays")
  public AlphabetWavesView(Context context, AttributeSet attrs, int defStyle) {
    super(context, attrs, defStyle);

    final Resources res = context.getResources();

    TypedArray a =
        context.obtainStyledAttributes(attrs, R.styleable.AlphabetWavesView, defStyle, 0);
    mMaxOffset = a.getDimensionPixelSize(R.styleable.AlphabetWavesView_leMaxOffset, 54);
    mMoveCount = a.getInteger(R.styleable.AlphabetWavesView_leMoveCount, 7);
    mPopAnimTime = a.getInteger(R.styleable.AlphabetWavesView_lePopAnimTime, 120);

    mToastBg = a.getDrawable(R.styleable.AlphabetWavesView_leAlphabetToastBg);
    mSelectedBg = a.getDrawable(R.styleable.AlphabetWavesView_leSelectedBg);

    mToastOffset =
        a.getDimensionPixelSize(
            R.styleable.AlphabetWavesView_leToastOffset,
            res.getDimensionPixelSize(R.dimen.le_awv_toast_offset));
    mToastTextSize =
        a.getDimensionPixelSize(
            R.styleable.AlphabetWavesView_leToastTextSize,
            res.getDimensionPixelSize(R.dimen.le_awv_toast_text_size));
    mAlphabetTextSize =
        a.getDimensionPixelSize(
            R.styleable.AlphabetWavesView_leAlphabetTextSize,
            res.getDimensionPixelSize(R.dimen.le_awv_alphabet_text_size));
    mAlphabetMaxOffset =
        a.getDimensionPixelSize(
            R.styleable.AlphabetWavesView_leAlphabetMaxOffset,
            res.getDimensionPixelSize(R.dimen.le_awv_alphabet_max_offset));
    mPaddingTopBottom =
        a.getDimensionPixelSize(
            R.styleable.AlphabetWavesView_lePaddingTopBottom,
            res.getDimensionPixelSize(R.dimen.le_awv_padding_top_bottom));
    mAlphabetLeftMargin =
        a.getDimensionPixelSize(
            R.styleable.AlphabetWavesView_leAlphabetLeftMargin,
            res.getDimensionPixelSize(R.dimen.le_awv_alphabet_left_margin));
    a.recycle();

    if (mToastBg == null) {
      mToastBg = res.getDrawable(R.drawable.le_alphabet_toast_bg);
    }
    if (mSelectedBg == null) {
      mSelectedBg = res.getDrawable(R.drawable.le_alphabet_selected_bg);
    }

    mViewWidth = res.getDimensionPixelSize(R.dimen.le_awv_width);

    mHandler = new GestureHandler();
    mAlphabetList = new ArrayList<Alphabet>();
    mAnimMap = new HashMap<Integer, ValueAnimator>();

    mSelectedRect.set(0, 0, mSelectedBg.getIntrinsicWidth(), mSelectedBg.getIntrinsicHeight());
    isShowSelected = true;

    mPreviewImage = new ImageView(context);
    mPreviewImage.setMinimumWidth(mToastBg.getIntrinsicWidth());
    mPreviewImage.setMinimumHeight(mToastBg.getIntrinsicHeight());
    mPreviewImage.setBackground(mToastBg);
    mPreviewImage.setAlpha(0f);

    final int textMinSize = Math.max(0, mToastBg.getIntrinsicHeight() - 0);
    mPrimaryText = createPreviewTextView();
    mPrimaryText.setMinimumWidth(textMinSize);
    mPrimaryText.setMinimumHeight(textMinSize);

    mSecondaryText = createPreviewTextView();
    mSecondaryText.setMinimumWidth(textMinSize);
    mSecondaryText.setMinimumHeight(textMinSize);

    setGravity(Gravity.CENTER);
    setPadding(0, mPaddingTopBottom, 0, mPaddingTopBottom);
  }

  private static final Interpolator mInterpolator =
      new Interpolator() {
        public float getInterpolation(float t) {
          t -= 1.0f;
          return t * t * t * t * t + 1.0f;
        }
      };

  @SuppressLint("HandlerLeak")
  private class GestureHandler extends Handler {
    GestureHandler() {
      super();
    }

    @Override
    public void handleMessage(Message msg) {
      switch (msg.what) {
        case LONG_SELECT:
          mInSelect = true;
          popAlphabet();
          break;
        default:
          throw new RuntimeException("Unknown message " + msg); // never
      }
    }
  }

  private class Alphabet {
    String firstAlphabet;
    // int position; // unused
  }

  public static interface OnAlphabetListener {
    /**
     * When alphabet is changed
     *
     * @param alphabetPosition position in List
     * @param firstAlphabet the first alphabet
     */
    void onAlphabetChanged(int alphabetPosition, String firstAlphabet);
  }

  /**
   * set alphabet listener
   *
   * @param aListener
   */
  public void setOnAlphabetListener(OnAlphabetListener aListener) {
    mAlphabetListener = aListener;
  }

  /**
   * The incoming string List, clip each initials, to be displayed.
   *
   * @param listStr the list contains initial alphabet.
   */
  public void setAlphabetList(List<String> listStr) {
    mAlphabetList.clear();
    List<String> countries = listStr;
    // Don't sort inside.
    // Collections.sort(countries);

    String previousAlphabet = null;
    Pattern numberPattern = Pattern.compile("[0-9]");

    for (String country : countries) {
      String firstAlphabet = mIsClipInitial ? country.substring(0, 1) : country;

      // Group numbers together in the scroller
      if (mIsClipInitial && numberPattern.matcher(firstAlphabet).matches()) {
        firstAlphabet = "#";
      }

      // If we've changed to a new Alphabet, add the previous Alphabet to the alphabet scroller
      if (previousAlphabet != null && !firstAlphabet.equals(previousAlphabet)) {
        String tempLeter =
            mIsClipInitial ? previousAlphabet.toUpperCase(Locale.UK) : previousAlphabet;
        Alphabet alphabet = new Alphabet();
        alphabet.firstAlphabet = tempLeter;
        mAlphabetList.add(alphabet);
        // alphabet.position = mAlphabetList.size() - 1;
      }

      previousAlphabet = firstAlphabet;
    }

    if (previousAlphabet != null) {
      // Save the last Alphabet
      Alphabet alphabet = new Alphabet();
      alphabet.firstAlphabet = previousAlphabet;
      mAlphabetList.add(alphabet);
      // alphabet.position = mAlphabetList.size() - 1;
    }

    isSetList = true;
    requestLayout();
  }

  /** Removes this FastScroller overlay from the host view. */
  public void remove() {
    mOverlay.remove(mPreviewImage);
    mOverlay.remove(mPrimaryText);
    mOverlay.remove(mSecondaryText);
  }

  /** Measures and layouts the scrollbar and decorations. */
  public void updatePopAlphabetLayout() {
    // Prevent re-entry when RTL properties change as a side-effect of
    // resolving padding.
    if (mUpdatingLayout) {
      return;
    }

    mUpdatingLayout = true;

    updateContainerRect();

    final Rect bounds = mTempBounds;
    measurePreview(mPrimaryText, bounds);
    applyLayout(mPrimaryText, bounds);
    measurePreview(mSecondaryText, bounds);
    applyLayout(mSecondaryText, bounds);

    if (mPreviewImage != null) {
      // Apply preview image padding.
      bounds.left -= mPreviewImage.getPaddingLeft();
      bounds.top -= mPreviewImage.getPaddingTop();
      bounds.right += mPreviewImage.getPaddingRight();
      bounds.bottom += mPreviewImage.getPaddingBottom();
      applyLayout(mPreviewImage, bounds);
    }
  }

  /**
   * setSelection, if user is touching on the AWView, the function is invalid.
   *
   * @param index
   */
  public void setSelection(int index) {
    if (index >= 0 && index < mAlphabetList.size()) {
      TextView textV = (TextView) getChildAt(index);
      TextView curTextV = (TextView) getChildAt(mCurrentSelection);

      ColorStateList oldColor = null;
      if (textV != null) {
        oldColor = textV.getTextColors();
      }
      if (curTextV != null && oldColor != null) {
        curTextV.setTextColor(oldColor); // restore
      }
      mCurrentSelection = index;
      if (textV != null && oldColor != null) {
        if (isPoped) {
          textV.setTextColor(oldColor);
        } else {
          textV.setTextColor(Color.WHITE);
        }
      }
      if (!isPoped) {
        isShowSelected = true;
        invalidate();
      }

      if (mShowingPreview) {
        setState(STATE_CHANGE_TEXT);
        setState(STATE_NONE);
      }
    }
  }

  /**
   * Set toast color.
   *
   * @param argb The color used to fill the shape
   */
  public void setToastBackGroundColor(int argb) {
    if (mToastBg instanceof GradientDrawable) {
      GradientDrawable gDrawable = (GradientDrawable) mToastBg;
      gDrawable.setColor(argb);
    }
  }

  /**
   * Set selected color.
   *
   * @param argb The color used to fill the shape
   */
  public void setSelectedBackGroundColor(int argb) {
    if (mSelectedBg instanceof GradientDrawable) {
      GradientDrawable gDrawable = (GradientDrawable) mSelectedBg;
      gDrawable.setColor(argb);
    }
  }

  /**
   * setIsClipInitial
   *
   * @param isClipInitial clip is true, no clip is false.
   */
  public void setIsClipInitial(boolean isClipInitial) {
    mIsClipInitial = isClipInitial;
  }

  /** @return isClipIntial */
  public boolean isClipIntial() {
    return mIsClipInitial;
  }

  @Override
  public boolean onTouchEvent(MotionEvent event) {
    final boolean pointerUp =
        (event.getAction() & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_POINTER_UP;
    final int skipIndex = pointerUp ? event.getActionIndex() : -1;

    float sumX = 0, sumY = 0;
    final int count = event.getPointerCount();
    for (int i = 0; i < count; i++) {
      if (skipIndex == i) continue;
      sumX += event.getX(i);
      sumY += event.getY(i);
    }
    final int div = pointerUp ? count - 1 : count;
    final float focusX = sumX / div;
    final float focusY = sumY / div;
    getLocalVisibleRect(mTempRect);
    if (focusY < getPaddingTop()
        || focusY > getHeight() - getPaddingBottom()
        || !mTempRect.contains((int) focusX, (int) focusY)) {
      isShowSelected = true;
      invalidate();
      mHandler.removeMessages(LONG_SELECT);
      backAlphabet(true); // back selected alphabet.
      mAnimMap.clear();
      setState(STATE_NONE);
      if (!isPoped) {
        isShowSelected = true;
        invalidate();
      }
      changeTextColor(mCurrentSelection, true);
      return false;
    }

    if (mCurrentSelection != mPreSelection && event.getAction() == MotionEvent.ACTION_MOVE) {
      changeTextColor(mCurrentSelection, false);
    }

    switch (event.getAction() & MotionEvent.ACTION_MASK) {
      case MotionEvent.ACTION_DOWN:
        mSideIndexX = mLastFocusX = focusX;
        mSideIndexY = mLastFocusY = focusY;
        mInSelect = false;
        selectAlphabet();
        isShowSelected = false;
        int itemPosition = getItemPosition();
        if (mPreSelection == itemPosition) {
          changeTextColor(mCurrentSelection, false);
        }
        invalidate();
        isPoped = false;
        if (mAlphabetList.size() >= POP_LIMIT_NUM) {
          mHandler.removeMessages(LONG_SELECT);
          mHandler.sendEmptyMessageAtTime(
              LONG_SELECT,
              event.getDownTime()
                  + TAP_TIMEOUT /*+ LONGPRESS_TIMEOUT*/); // same as LONGPRESS_TIMEOUT
        }
        break;

      case MotionEvent.ACTION_MOVE:
        final float scrollX = mLastFocusX - focusX;
        final float scrollY = mLastFocusY - focusY;
        if (!mInSelect) {
          if ((Math.abs(scrollX) >= 1) || (Math.abs(scrollY) >= 1)) {
            mSideIndexX = mSideIndexX - scrollX;
            mSideIndexY = mSideIndexY - scrollY;
            mLastFocusX = focusX;
            mLastFocusY = focusY;
            if (mSideIndexX >= 0 && mSideIndexY >= 0) {
              isShowSelected = false;
              invalidate();
              selectAlphabet();
            }
            break;
          }
        }
        if ((Math.abs(scrollX) >= 1) || (Math.abs(scrollY) >= 1)) {
          mSideIndexX = mSideIndexX - scrollX;
          mSideIndexY = mSideIndexY - scrollY;
          mLastFocusX = focusX;
          mLastFocusY = focusY;
          isShowSelected = false;
          invalidate();
          if (mSideIndexX >= 0 && mSideIndexY >= 0) {
            if (selectAlphabet()) {
              popAlphabet();
            }
          }
        }
        break;
      case MotionEvent.ACTION_UP:
      case MotionEvent.ACTION_CANCEL:
        if (mInSelect) {
          mInSelect = false;
        }
        isShowSelected = true;
        invalidate();
        mHandler.removeMessages(LONG_SELECT);

        backAlphabet(true); // back selected alphabet.
        changeTextColor(mCurrentSelection, true);
        isPoped = false; // PS: mBackAnimTime = 0

        mAnimMap.clear();
        setState(STATE_NONE);
        break;
      default:
        break;
    }
    return true;
  }

  @Override
  protected void dispatchDraw(Canvas canvas) {
    View childView = getChildAt(mCurrentSelection);
    TextView childTextView = null;
    if (childView instanceof TextView) {
      childTextView = (TextView) childView;
    }

    if (childTextView != null
        && !TextUtils.isEmpty(childTextView.getText())
        && getVisibility() == View.VISIBLE
        && childTextView.getVisibility() == View.VISIBLE) {
      Rect childRect = new Rect();
      childTextView.getLocalVisibleRect(childRect);

      float childViewX = childTextView.getX() + childTextView.getWidth() / 2;
      float childViewY = childTextView.getY() + childTextView.getHeight() / 2;

      int top = (int) (childViewY - mSelectedRect.height() / 2);
      int left = (int) (childViewX - mSelectedRect.width() / 2);

      mSelectedRect.set(left, top, left + mSelectedRect.width(), top + mSelectedRect.height());

      if (isShowSelected && !mSelectedRect.isEmpty()) {
        final Drawable selectedBg = mSelectedBg;
        selectedBg.setBounds(mSelectedRect);
        selectedBg.draw(canvas);
      }
    }

    super.dispatchDraw(canvas);
  }

  /** Pop alphabet */
  protected void popAlphabet() {
    if (mAlphabetList.size() < POP_LIMIT_NUM) {
      return;
    }
    backAlphabet(false); // first, back alphabet, and pop alphabet

    isPoped = true;
    changeTextColor(mCurrentSelection, false);
    isShowSelected = false;
    invalidate();

    int position;
    int halfMoveCount = (mMoveCount + 1) / 2;
    for (int i = 0; i < mMoveCount; i++) {
      position = mCurrentSelection - halfMoveCount + 1 + i;
      if (position >= 0 && position < getChildCount()) {
        View view = getChildAt(position);
        ValueAnimator tmpAnimator =
            ObjectAnimator.ofFloat(
                view,
                "translationX",
                view.getTranslationX(),
                -mMaxOffset * (float) Math.sin((i + 1) * Math.PI / (mMoveCount + 1)));
        // Math.sin((i + 1) * Math.PI / (mMoveCount + 2))区间是[0,1],如移动的字母数(mMoveCount=3),
        // 那么取sin曲线上5个点,1和5点无动画,只创建中间3点动画,值是0.7071、1.0、0.7071。
        tmpAnimator.setDuration(mPopAnimTime);
        tmpAnimator.setRepeatCount(0);
        tmpAnimator.setInterpolator(mInterpolator);
        tmpAnimator.start();
        mAnimMap.put(position, tmpAnimator);
      }
    }
  }

  /**
   * Back alphabet.
   *
   * @param isSelected 是否是选中的字母周围弹出的字母
   */
  protected void backAlphabet(boolean isSelected) {
    if (mAlphabetList.size() < POP_LIMIT_NUM) {
      return;
    }

    int halfMoveCount = (mMoveCount + 1) / 2;
    for (int i = 0; i < mAlphabetList.size(); i++) {
      ValueAnimator vaAnim = mAnimMap.get(i);
      if (vaAnim != null) {
        vaAnim.cancel();
      }
      // Back around the selected alphabet place.
      if (isSelected
          && i > mCurrentSelection - halfMoveCount
          && i < mCurrentSelection + halfMoveCount) {
        View view = getChildAt(i);

        float tX = view.getTranslationX();
        if (tX < 0f || tX > 0f) {
          doBackAnim(view);
        }
        // Back the unselected alphabet place.
      } else if (i <= mCurrentSelection - halfMoveCount || i >= mCurrentSelection + halfMoveCount) {
        View view = getChildAt(i);

        float tX = view.getTranslationX();
        if (tX < 0f || tX > 0f) {
          doBackAnim(view);
        }
      }
    }
  }

  private int getItemPosition() {
    mSideIndexHeight =
        getHeight() - 2 * getPaddingTop(); // paddingTop is variational.see updateView()
    // compute number of pixels for every side index item
    double pixelPerIndexItem = (double) mSideIndexHeight / mIndexListSize;
    // compute the item index for given event position belongs to
    int itemPosition = (int) ((mSideIndexY - getPaddingTop()) / pixelPerIndexItem);
    return itemPosition;
  }

  protected boolean selectAlphabet() {
    final int itemPosition = getItemPosition();
    // get the item (we can do it since we know item index)
    if (itemPosition < mAlphabetList.size() && mCurrentSelection != itemPosition) {
      mPreSelection = mCurrentSelection;
      String firstAlphabet = mAlphabetList.get(itemPosition).firstAlphabet;

      TextView textV = (TextView) getChildAt(itemPosition); // current
      TextView curTextV = (TextView) getChildAt(mCurrentSelection); // pre

      if (curTextV != null && textV != null) {
        curTextV.setTextColor(textV.getTextColors()); // restore
      }
      mCurrentSelection = itemPosition;

      if (textV != null) {
        textV.setTextColor(Color.WHITE);
      }

      if (mShowingPreview) {
        setState(STATE_CHANGE_TEXT);
      } else {
        setState(STATE_VISIBLE);
      }

      // notify alphabet changed.
      if (mAlphabetListener != null) {
        mAlphabetListener.onAlphabetChanged(itemPosition, firstAlphabet);
      }
      return true;
    }
    return false;
  }

  @Override
  protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    super.onSizeChanged(w, h, oldw, oldh);
    ViewGroup viewGroup = (ViewGroup) getParent();
    mOverlay = viewGroup.getOverlay();

    mOverlay.add(mPreviewImage);
    mOverlay.add(mPrimaryText);
    mOverlay.add(mSecondaryText);
  }

  @Override
  protected void onLayout(boolean changed, int l, int t, int r, int b) {
    super.onLayout(changed, l, t, r, b);
    if (isSetList) {
      adjustPadding();
      requestLayout();
      for (int i = 0; i < getChildCount(); i++) {
        View view = getChildAt(i);
        view.requestLayout();
      }
      isPoped = false; // Fix bug: when isPoped, trun off screen, them trun on screne the selected
      // alphabet is black.
      isSetList = false;
    }
    updatePopAlphabetLayout();
    if (!isPoped) {
      changeTextColor(mCurrentSelection, true);
    }
  }

  @Override
  protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    if (isSetList) {
      mIndexListSize = mAlphabetList.size();
      addTextView();
    }
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
  }

  private void setState(int state) {
    removeCallbacks(mDeferHide);

    switch (state) {
      case STATE_NONE:
        postAutoHide();
        break;
      case STATE_VISIBLE:
        String text = mAlphabetList.get(mCurrentSelection).firstAlphabet;
        mPrimaryText.setText(text);
        mSecondaryText.setText("");
        transitionToVisible();
        break;
      case STATE_CHANGE_TEXT:
        if (!transitionPreviewLayout(mCurrentSelection)) {
          transitionToHidden();
        }
        break;
    }
  }

  /** Used to delay hiding fast scroll decorations. */
  private final Runnable mDeferHide =
      new Runnable() {
        @Override
        public void run() {
          transitionToHidden();
        }
      };

  /**
   * Constructs an animator for the specified property on a group of views. See {@link
   * ObjectAnimator#ofFloat(Object, String, float...)} for implementation details.
   *
   * @param property The property being animated.
   * @param value The value to which that property should animate.
   * @param views The target views to animate.
   * @return An animator for all the specified views.
   */
  private static Animator groupAnimatorOfFloat(
      Property<View, Float> property, float value, View... views) {
    AnimatorSet animSet = new AnimatorSet();
    AnimatorSet.Builder builder = null;

    for (int i = views.length - 1; i >= 0; i--) {
      final Animator anim = ObjectAnimator.ofFloat(views[i], property, value);
      if (builder == null) {
        builder = animSet.play(anim);
      } else {
        builder.with(anim);
      }
    }

    return animSet;
  }

  /** Shows nothing. */
  private void transitionToHidden() {
    if (mDecorAnimation != null) {
      mDecorAnimation.cancel();
    }

    final Animator fadeOut =
        groupAnimatorOfFloat(View.ALPHA, 0f, mPreviewImage, mPrimaryText, mSecondaryText)
            .setDuration(DURATION_FADE_OUT);

    mDecorAnimation = new AnimatorSet();
    mDecorAnimation.playTogether(fadeOut);
    mDecorAnimation.start();

    mShowingPreview = false;
  }

  /** Shows the toast. */
  private void transitionToVisible() {
    if (mDecorAnimation != null) {
      mDecorAnimation.cancel();
    }

    final Animator fadeIn =
        groupAnimatorOfFloat(View.ALPHA, 1f, mPreviewImage, mPrimaryText, mSecondaryText)
            .setDuration(DURATION_FADE_IN);

    mDecorAnimation = new AnimatorSet();
    mDecorAnimation.playTogether(fadeIn);
    mDecorAnimation.start();

    mShowingPreview = true;
  }

  private void postAutoHide() {
    removeCallbacks(mDeferHide);
    postDelayed(mDeferHide, FADE_TIMEOUT);
  }

  private void addTextView() {
    removeAllViews();
    if (mIndexListSize < 1) {
      return;
    }

    TextView tmpTV;
    for (double i = 1; i <= mIndexListSize; i++) {
      String tmpAlphabet = mAlphabetList.get((int) i - 1).firstAlphabet;

      tmpTV = new TextView(getContext());
      tmpTV.setText(tmpAlphabet);
      tmpTV.setGravity(Gravity.CENTER);
      tmpTV.setTextSize(TypedValue.COMPLEX_UNIT_PX, mAlphabetTextSize);
      tmpTV.setTextColor(Color.BLACK);
      LinearLayout.LayoutParams params =
          new LinearLayout.LayoutParams(
              ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, 1);
      params.leftMargin = mAlphabetLeftMargin;
      tmpTV.setLayoutParams(params);
      tmpTV.setIncludeFontPadding(false);

      addView(tmpTV);
    }
  }

  private void adjustPadding() {
    int padding =
        (getHeight()
                - mAlphabetMaxOffset * (mIndexListSize - 1)
                - mAlphabetTextSize * mIndexListSize)
            / 2;
    if (padding > mPaddingTopBottom) {
      setPadding(getPaddingStart(), padding, getPaddingEnd(), padding);
    } else {
      setPadding(getPaddingStart(), mPaddingTopBottom, getPaddingEnd(), mPaddingTopBottom);
    }
  }

  /**
   * Transitions the preview text to a new section. Handles animation, measurement, and layout. If
   * the new preview text is empty, returns false.
   *
   * @param sectionIndex The section index to which the preview should transition.
   * @return False if the new preview text is empty.
   */
  private boolean transitionPreviewLayout(int sectionIndex) {
    String text = mAlphabetList.get(sectionIndex).firstAlphabet;

    final Rect bounds = mTempBounds;
    final TextView showing;
    final TextView target;
    if (mShowingPrimary) {
      showing = mPrimaryText;
      target = mSecondaryText;
    } else {
      showing = mSecondaryText;
      target = mPrimaryText;
    }

    // Set and layout target immediately.
    target.setText(text);
    measurePreview(target, bounds);
    applyLayout(target, bounds);

    if (mPreviewAnimation != null) {
      mPreviewAnimation.cancel();
    }

    // Cross-fade preview text.
    final Animator showTarget = animateAlpha(target, 1f).setDuration(DURATION_CROSS_FADE);
    final Animator hideShowing = animateAlpha(showing, 0f).setDuration(DURATION_CROSS_FADE);
    hideShowing.addListener(mSwitchPrimaryListener);

    // Apply preview image padding and animate bounds, if necessary.
    bounds.left -= mPreviewImage.getPaddingLeft();
    bounds.top -= mPreviewImage.getPaddingTop();
    bounds.right += mPreviewImage.getPaddingRight();
    bounds.bottom += mPreviewImage.getPaddingBottom();
    /*final Animator resizePreview = animateBounds(preview, bounds);
    resizePreview.setDuration(DURATION_RESIZE);*/

    mPreviewAnimation = new AnimatorSet();
    mPreviewAnimation.play(hideShowing).with(showTarget);
    mPreviewAnimation.start();

    return !TextUtils.isEmpty(text);
  }

  /** Used to effect a transition from primary to secondary text. */
  private final AnimatorListener mSwitchPrimaryListener =
      new AnimatorListenerAdapter() {
        @Override
        public void onAnimationEnd(Animator animation) {
          mShowingPrimary = !mShowingPrimary;
        }
      };

  /** Returns an animator for the view's alpha value. */
  private static Animator animateAlpha(View v, float alpha) {
    return ObjectAnimator.ofFloat(v, View.ALPHA, alpha);
  }

  /**
   * Measures the preview text bounds, taking preview image padding into account. This method should
   * only be called after {@link #layoutThumb()} and {@link #layoutTrack()} have both been called at
   * least once.
   *
   * @param v The preview text view to measure.
   * @param out Rectangle into which measured bounds are placed.
   */
  private void measurePreview(View v, Rect out) {
    // Apply the preview image's padding as layout margins.
    final Rect margins = mTempMargins;
    margins.left = mPreviewImage.getPaddingLeft();
    margins.top = mPreviewImage.getPaddingTop();
    margins.right = mPreviewImage.getPaddingRight();
    margins.bottom = mPreviewImage.getPaddingBottom();

    measureFloating(v, margins, out);
  }

  private void measureFloating(View preview, Rect margins, Rect out) {
    final int marginLeft;
    final int marginTop;
    final int marginRight;
    if (margins == null) {
      marginLeft = 0;
      marginTop = 0;
      marginRight = 0;
    } else {
      marginLeft = margins.left;
      marginTop = margins.top;
      marginRight = margins.right;
    }

    final Rect container = mContainerRect;
    final int containerWidth = container.width();
    final int adjMaxWidth = containerWidth - marginLeft - marginRight;
    final int widthMeasureSpec = MeasureSpec.makeMeasureSpec(adjMaxWidth, MeasureSpec.AT_MOST);
    final int heightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
    preview.measure(widthMeasureSpec, heightMeasureSpec);

    // Align at the vertical center, mToastOffset away from this View.
    final int containerHeight = container.height();
    final int width = preview.getMinimumWidth();
    final int top = (containerHeight - width) / 2 + container.top;
    final int bottom = top + preview.getMeasuredHeight();
    final int left = containerWidth - mViewWidth - mToastOffset - width + container.left;
    final int right = left + width;
    out.set(left, top, right, bottom);
  }

  /** Creates a view into which preview text can be placed. */
  private TextView createPreviewTextView() {
    final LayoutParams params =
        new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
    // final Resources res = getContext().getResources();
    // final float textSize = res.getDimensionPixelSize(R.dimen.fastscroll_overlay_text_size);
    final TextView textView = new TextView(getContext());
    textView.setLayoutParams(params);
    textView.setTextColor(Color.WHITE);
    textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, mToastTextSize);
    textView.setSingleLine(true);
    textView.setEllipsize(TruncateAt.MIDDLE);
    textView.setGravity(Gravity.CENTER);
    textView.setAlpha(0f);
    return textView;
  }

  /** Updates the container rectangle used for layout. */
  private void updateContainerRect() {
    /*ViewGroup viewGroup = (ViewGroup) getParent();
    final Rect container = mContainerRect;
    container.left = viewGroup.getLeft() + viewGroup.getPaddingLeft();
    container.top = viewGroup.getTop() + viewGroup.getPaddingTop();
    container.right = viewGroup.getRight() - viewGroup.getPaddingRight();
    container.bottom = viewGroup.getBottom() - viewGroup.getPaddingBottom();*/

    final Rect container = mContainerRect;
    container.left = getLeft() + getPaddingLeft();
    container.top = getTop() + getPaddingTop();
    container.right = getRight() - getPaddingRight();
    container.bottom = getBottom() - getPaddingBottom();
  }

  /**
   * Layouts a view within the specified bounds and pins the pivot point to the appropriate edge.
   *
   * @param view The view to layout.
   * @param bounds Bounds at which to layout the view.
   */
  private void applyLayout(View view, Rect bounds) {
    view.layout(bounds.left, bounds.top, bounds.right, bounds.bottom);
    view.setPivotX(bounds.right - bounds.left);
  }

  private void doBackAnim(final View view) {
    ValueAnimator tmpAnimator =
        ObjectAnimator.ofFloat(view, "translationX", view.getTranslationX(), 0);
    tmpAnimator.setDuration(mBackAnimTime);
    tmpAnimator.setRepeatCount(0);
    tmpAnimator.start();
  }

  private void changeTextColor(int position, boolean isWhite) {
    TextView curTextV = (TextView) getChildAt(position);
    if (curTextV != null) {
      curTextV.setTextColor(isWhite ? Color.WHITE : Color.BLACK);
    }
  }
}