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.