The project address

About Android to achieve iOS wheel selection effect controls, to github a lot of search, the reason for building this wheel, the purpose is to better learn the custom control, this control is written a few months ago, after a period of improvement, now open source, incidentally write this simple introduction article. The effect is as follows, the screen recording software may look a little bit stuck, specific can download the source code to run:

  • measure

    The measurement process is relatively simple, using the desired size of the text, plus the padding.

    @Override
      protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
          super.onMeasure(widthMeasureSpec, heightMeasureSpec);
          int wantWith = getPaddingLeft() + getPaddingRight();
          int wantHeight = getPaddingTop() + getPaddingBottom();
          calculateTextSize();
          wantWith += mTextRect.width();
          // The number of visible items counts the size of the text
          if (mVisibilityCount > 0) {
              wantHeight += mTextRect.height() * mVisibilityCount;
          } else {
              wantHeight += mTextRect.height() * DEFALUT_VISIBILITY_COUNT;
          }
          setMeasuredDimension(
                  resolveSize(wantWith, widthMeasureSpec),
                  resolveSize(wantHeight, heightMeasureSpec)
          );
          mNeedCalculate = true;
      }
    Copy the code
  • draw

    The drawing process is to draw components in different positions, including text content and selection boxes, through the displacement of canvas. It may be noted that it is not necessary to draw all the text at one time, but only the visible text.

    @Override
      protected void onDraw(Canvas canvas) {
          super.onDraw(canvas);
    
          if (hasDataSource()) {
              / / to omit
              +2 is just to make sure there is no blank space
              final int drawCount = mContentRect.height() / mTextRect.height() + 2;
              int invisibleCount = 0;
              int dy = -mDistanceY;
              / / to omit
              // Draw text with Translate
              for (int i = 0; (i < drawCount && mDataSources.size() > (invisibleCount + i));
                   i++) {
                  final int position = invisibleCount + i;
                  String text = mDataSources.get(position);
                  if (i > 0) {
                      canvas.translate(0, mTextRect.height());
                  }
    
                  final PointF pointF = calculateTextGravity(text);
                  mTextPaint.setTextSize(mTextSize);
                  if (position == selctPosition) {
                      mTextPaint.setColor(mSelectedTextColor);
                  } else {
                      mTextPaint.setColor(mNormalTextColor);
                  }
                  canvas.drawText(text, pointF.x, pointF.y, mTextPaint);
              }
              canvas.restoreToCount(saveCount);
          }
          
          // Draw the selection box
          int saveCount = canvas.save();
          mDrawPaint.setColor(mSelectedLineColor);
          canvas.translate(mContentRect.left, mContentRect.top);
          canvas.drawLine(
                  mSelctedRect.left,
                  mSelctedRect.top,
                  mSelctedRect.right,
                  mSelctedRect.top,
                  mDrawPaint
          );
          canvas.drawLine(
                  mSelctedRect.left,
                  mSelctedRect.bottom,
                  mSelctedRect.right,
                  mSelctedRect.bottom,
                  mDrawPaint
          );
          canvas.restoreToCount(saveCount);
      }
    Copy the code
  • layout

    Because this control inherits from the View, it does not need to deal with onLayout.

  • touch

    If you are familiar with the Touch Event distribution process, much of the processing can be described as template code, see NestedScrollView, ScrollView.

    On the onInterceptTouchEvent, save the dragging gesture to the variable misbeingPartying:

    // Multipoint processing
    final int pointerIndex = ev.findPointerIndex(activePointerId);
                  if (pointerIndex == -1) {
                      Log.e(TAG, "Invalid pointerId=" + activePointerId
                              + " in onInterceptTouchEvent");
                      break;
                  }
    
                  final int y = (int) ev.getY(pointerIndex);
                  final int yDiff = Math.abs(y - mLastMotionY);
                  if (yDiff > mTouchSlop && (getNestedScrollAxes() & SCROLL_AXIS_VERTICAL) == 0) {
                      // Start dragging
                      mIsBeingDragged = true;
                      mLastMotionY = y;
                      initVelocityTrackerIfNotExists();
                      mVelocityTracker.addMovement(ev);
                      mNestedYOffset = 0;
                      if (mScrollStrictSpan == null) {
                          mScrollStrictSpan = StrictMode.enterCriticalSpan("ScrollView-scroll");
                      }
                      final ViewParent parent = getParent();
                      if(parent ! =null) {
                          // Disallows the parent control from intercepting event distribution
                          parent.requestDisallowInterceptTouchEvent(true); }}Copy the code

    ACTION_MOVR drag is handled in onTouchEvent, and nested scrolling is pre-distributed if nested scrolling is supported. If shadow effects are supported, use EdgeEffect.

    // Just like onInterceptTouchEvent, check the start of the drag gesture
    if(! mIsBeingDragged && Math.abs(deltaY) > mTouchSlop) {final ViewParent parent = getParent();
                      if(parent ! =null) {
                          parent.requestDisallowInterceptTouchEvent(true);
                      }
                      mIsBeingDragged = true;
                      if (deltaY > 0) {
                          deltaY -= mTouchSlop;
                      } else{ deltaY += mTouchSlop; }}if (mIsBeingDragged) {
                      // Drag processing
                      // Scroll to follow the motion event
                      mLastMotionY = y - mScrollOffset[1];
    
                      final int oldY = mScrollY;
                      final int range = getScrollRange();
                      final int overscrollMode = getOverScrollMode();
                      boolean canOverscroll = overscrollMode == OVER_SCROLL_ALWAYS ||
                              (overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0);
    
                      // Calling overScrollBy will call onOverScrolled, which
                      // calls onScrollChanged if applicable. 
                      OverScrollBy handles nested scroll predistribution
                      if (overScrollBy(0, deltaY, 0, mScrollY, 0, range, 0, mOverscrollDistance, true)
                              && !hasNestedScrollingParent()) {
                          // Break our velocity if we hit a scroll barrier.
                          mVelocityTracker.clear();
                      }
    
                      final int scrolledDeltaY = mScrollY - oldY;
                      final int unconsumedY = deltaY - scrolledDeltaY;
                      // nested scroll
                      if (dispatchNestedScroll(0, scrolledDeltaY, 0, unconsumedY, mScrollOffset)) {
                          mLastMotionY -= mScrollOffset[1];
                          vtev.offsetLocation(0, mScrollOffset[1]);
                          mNestedYOffset += mScrollOffset[1];
                      } else if (canOverscroll) {
                          final int pulledToY = oldY + deltaY;
                          // Drag the shadow effect
                          if (pulledToY < 0) {
                              mEdgeGlowTop.onPull((float) deltaY / getHeight(),
                                      ev.getX(activePointerIndex) / getWidth());
                              if (!mEdgeGlowBottom.isFinished()) {
                                  mEdgeGlowBottom.onRelease();
                              }
                          } else if (pulledToY > range) {
                              mEdgeGlowBottom.onPull((float) deltaY / getHeight(),
                                      1.f - ev.getX(activePointerIndex) / getWidth());
                              if (!mEdgeGlowTop.isFinished()) {
                                  mEdgeGlowTop.onRelease();
                              }
                          }
                          if(mEdgeGlowTop ! =null
                                  && (!mEdgeGlowTop.isFinished() || !mEdgeGlowBottom.isFinished())) {
                              postInvalidateOnAnimation();
                          }
                      }
                  }
    Copy the code

    Widgets that support scrolling generally support the fling gesture, which is inertial scrolling. This is also template code that analyzes drag speed in ACTION_UP in onTouchEvent.

    case MotionEvent.ACTION_UP:
                  if (mIsBeingDragged) {
                      final VelocityTracker velocityTracker = mVelocityTracker;
                      velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
                      // Get the drag speed
                      int initialVelocity = (int) velocityTracker.getYVelocity(mActivePointerId);
    
                      if ((Math.abs(initialVelocity) > mMinimumVelocity)) {
                          // The fling operation can be performed
                          flingWithNestedDispatch(-initialVelocity);
                      } else if (mScroller.springBack(mScrollX, mScrollY, 0.0.0,
                              getScrollRange())) {
                          postInvalidateOnAnimation();
                      }
    
                      mActivePointerId = INVALID_POINTER;
                      endDrag();
                  }
                  break;
    Copy the code

    The code can be read in ScrollView.

    Going back to the custom controls I implemented, the code for handling touch events is basically the same as for system controls. After getting the drag distance, draw the visible areas at different locations based on that value. There are two more treatments:

    After the first drag, reset. After the drag, if the selection box stays between two items, select the item closer according to the distance between the two items.

    private void correctionDistanceY(a) {
          if(mDistanceY % mTextRect.height() ! =0) {
              int position = mDistanceY / mTextRect.height();
              int remainder = mDistanceY % mTextRect.height();
              if (remainder >= mTextRect.height() / 2f) {
                  position++;
              }
              intnewDistanceY = position * mTextRect.height(); animChangeDistanceY(newDistanceY); }}Copy the code

    The second problem is found in the use of Scroller. If the remaining scrolling distance is too short and the dragging speed is too fast, the fling process will not end and the vision will not change. At the same time, the selection callback is performed after the end of the scroll, so the physical examination is not good. Therefore, copy the method of calculating duration in Scroller, calculate the appropriate duration according to the remaining scrolls, and manually interrupt the Scroller fling processing.

    if ((SystemClock.elapsedRealtime() - mStartFlingTime) >= mFlingDuration || currY == mScroller.getFinalY()) {
                  //duration or current == final
                  if (DEBUG) {
                      Logger.d("abortAnimation");
                  }
                  mScroller.abortAnimation();
              }
    Copy the code

Specific code can read the source code.