@Override
  protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    super.onLayout(changed, left, top, right, bottom);

    // Target placement width/height. This puts the targets on the greater of the ring
    // width or the specified outer radius.
    final float placementWidth = getRingWidth();
    final float placementHeight = getRingHeight();
    float newWaveCenterX = mHorizontalInset + (mMaxTargetWidth + placementWidth) / 2;
    float newWaveCenterY = mVerticalInset + (mMaxTargetHeight + placementHeight) / 2;

    if (mInitialLayout) {
      stopAndHideWaveAnimation();
      hideTargets(false, false);
      mInitialLayout = false;
    }

    mOuterRing.setPositionX(newWaveCenterX);
    mOuterRing.setPositionY(newWaveCenterY);

    mPointCloud.setScale(mRingScaleFactor);

    mHandleDrawable.setPositionX(newWaveCenterX);
    mHandleDrawable.setPositionY(newWaveCenterY);

    updateTargetPositions(newWaveCenterX, newWaveCenterY);
    updatePointCloudPosition(newWaveCenterX, newWaveCenterY);
    updateGlowPosition(newWaveCenterX, newWaveCenterY);

    mWaveCenterX = newWaveCenterX;
    mWaveCenterY = newWaveCenterY;

    if (DEBUG) dump();
  }
 private void handleDown(MotionEvent event) {
   int actionIndex = event.getActionIndex();
   float eventX = event.getX(actionIndex);
   float eventY = event.getY(actionIndex);
   switchToState(STATE_START, eventX, eventY);
   if (!trySwitchToFirstTouchState(eventX, eventY)) {
     mDragging = false;
   } else {
     mPointerId = event.getPointerId(actionIndex);
     updateGlowPosition(eventX, eventY);
   }
 }
 private boolean trySwitchToFirstTouchState(float x, float y) {
   final float tx = x - mWaveCenterX;
   final float ty = y - mWaveCenterY;
   if (mAlwaysTrackFinger || dist2(tx, ty) <= getScaledGlowRadiusSquared()) {
     if (DEBUG) Log.v(TAG, "** Handle HIT");
     switchToState(STATE_FIRST_TOUCH, x, y);
     updateGlowPosition(tx, ty);
     mDragging = true;
     return true;
   }
   return false;
 }
  private void handleMove(MotionEvent event) {
    int activeTarget = -1;
    final int historySize = event.getHistorySize();
    ArrayList<TargetDrawable> targets = mTargetDrawables;
    int ntargets = targets.size();
    float x = 0.0f;
    float y = 0.0f;
    float activeAngle = 0.0f;
    int actionIndex = event.findPointerIndex(mPointerId);

    if (actionIndex == -1) {
      return; // no data for this pointer
    }

    for (int k = 0; k < historySize + 1; k++) {
      float eventX =
          k < historySize ? event.getHistoricalX(actionIndex, k) : event.getX(actionIndex);
      float eventY =
          k < historySize ? event.getHistoricalY(actionIndex, k) : event.getY(actionIndex);
      // tx and ty are relative to wave center
      float tx = eventX - mWaveCenterX;
      float ty = eventY - mWaveCenterY;
      float touchRadius = (float) Math.sqrt(dist2(tx, ty));
      final float scale = touchRadius > mOuterRadius ? mOuterRadius / touchRadius : 1.0f;
      float limitX = tx * scale;
      float limitY = ty * scale;
      double angleRad = Math.atan2(-ty, tx);

      if (!mDragging) {
        trySwitchToFirstTouchState(eventX, eventY);
      }

      if (mDragging) {
        // For multiple targets, snap to the one that matches
        final float snapRadius = mRingScaleFactor * mOuterRadius - mSnapMargin;
        final float snapDistance2 = snapRadius * snapRadius;
        // Find first target in range
        for (int i = 0; i < ntargets; i++) {
          TargetDrawable target = targets.get(i);

          double targetMinRad = mFirstItemOffset + (i - 0.5) * 2 * Math.PI / ntargets;
          double targetMaxRad = mFirstItemOffset + (i + 0.5) * 2 * Math.PI / ntargets;
          if (target.isEnabled()) {
            boolean angleMatches =
                (angleRad > targetMinRad && angleRad <= targetMaxRad)
                    || (angleRad + 2 * Math.PI > targetMinRad
                        && angleRad + 2 * Math.PI <= targetMaxRad)
                    || (angleRad - 2 * Math.PI > targetMinRad
                        && angleRad - 2 * Math.PI <= targetMaxRad);
            if (angleMatches && (dist2(tx, ty) > snapDistance2)) {
              activeTarget = i;
              activeAngle = (float) -angleRad;
            }
          }
        }
      }
      x = limitX;
      y = limitY;
    }

    if (!mDragging) {
      return;
    }

    if (activeTarget != -1) {
      switchToState(STATE_SNAP, x, y);
      updateGlowPosition(x, y);
    } else {
      switchToState(STATE_TRACKING, x, y);
      updateGlowPosition(x, y);
    }

    if (mActiveTarget != activeTarget) {
      // Defocus the old target
      if (mActiveTarget != -1) {
        TargetDrawable target = targets.get(mActiveTarget);
        if (target.hasState(TargetDrawable.STATE_FOCUSED)) {
          target.setState(TargetDrawable.STATE_INACTIVE);
        }
        if (mMagneticTargets) {
          updateTargetPosition(mActiveTarget, mWaveCenterX, mWaveCenterY);
        }
      }
      // Focus the new target
      if (activeTarget != -1) {
        TargetDrawable target = targets.get(activeTarget);
        if (target.hasState(TargetDrawable.STATE_FOCUSED)) {
          target.setState(TargetDrawable.STATE_FOCUSED);
        }
        if (mMagneticTargets) {
          updateTargetPosition(activeTarget, mWaveCenterX, mWaveCenterY, activeAngle);
        }
        if (AccessibilityManager.getInstance(mContext).isEnabled()) {
          String targetContentDescription = getTargetDescription(activeTarget);
          announceForAccessibility(targetContentDescription);
        }
      }
    }
    mActiveTarget = activeTarget;
  }