First on a GIF, the meeting room appointment time selection, the whole control custom view way to achieve, the view control slide left and right, finger lift inertia slide, select a single time can be dragged on a single selected area, select multiple areas can click the icon to slide left and right selection

Code analysis:

In the custom control, it is necessary to rewrite the View onMeasure() and onDraw() methods. The onMeasure method mainly determines the width and height of the current control. Here we should pay attention to the specified width and height value should match the match_parent of the system Wrap_content and the padding value of the control

// @override protected void onMeasure(int); // @override protected void onMeasure(int) widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); final int minimumWidth = getSuggestedMinimumWidth(); final int minimumHeight = getSuggestedMinimumHeight(); int width = measureWidth(minimumWidth, widthMeasureSpec); int height = measureHeight(minimumHeight, heightMeasureSpec); setMeasuredDimension(width, height); mMeasuredWidth = getMeasuredWidth(); mMeasuredHeight = getMeasuredHeight(); limitOffset = mDataArea.length * areaWidth - mMeasuredWidth; }Copy the code

Note that the padding value should be taken into account when measuring the height and width. When measuring, the maximum width of the control should be less than or equal to the maximum value occupied by the current control in the parent control. Otherwise, it will affect our customized sliding operation

Height measurement of * * * * * / / / @ param defaultHeight * @ param measureSpec * @ return * / private int measureWidth (int defaultWidth, int measureSpec) { int specMode = MeasureSpec.getMode(measureSpec); int specSize = MeasureSpec.getSize(measureSpec); Int defaultWidth1 = (int) (mDataArea.length * areaWidth + getPaddingLeft() + getPaddingRight()); Case MeasureSpec.AT_MOST://wrap defaultWidth = defaultWidth1 > getMeasuredWidth()? getMeasuredWidth() : defaultWidth1; break; Case MeasureSpec. EXACTLY: / / match and precise numerical defaultWidth = defaultWidth1 > specSize? specSize : defaultWidth1; break; case MeasureSpec.UNSPECIFIED: defaultWidth = Math.max(defaultWidth, specSize); default: break; } return defaultWidth; } /** */ / measureHeight ** @measurespec * @measurespec */ private int measureHeight(int measureHeight, int measureSpec) { int specMode = MeasureSpec.getMode(measureSpec); int specSize = MeasureSpec.getSize(measureSpec); switch (specMode) { case MeasureSpec.AT_MOST: defaultHeight = (int) (-mTimePaint.ascent() + mTimePaint.descent()) * 6 + getPaddingTop() + getPaddingBottom(); break; case MeasureSpec.EXACTLY: defaultHeight = specSize; break; case MeasureSpec.UNSPECIFIED: defaultHeight = Math.max(mTextsize * 6, specSize); break; default: break; } return defaultHeight; }Copy the code

Next to rendering of content, text and line drawing first, after drawing good can only display a screen of data, at this point we have to consider is how to make the control slip up and the preliminary idea is to record his fingers down to lift the sliding distance on ontouch () around the canvas do translation operations to achieve sliding effect

canvas.save(); Canvas. Translate (mOffset + mTempset, 0); for (int i = 0; i < mDataArea.length; i++) { if (i ! = 0) { canvas.drawLine(areaWidth * i + getPaddingLeft(), getPaddingTop(), areaWidth * i + getPaddingLeft(), mMeasuredHeight - getPaddingBottom(), mLinePaint); } canvas.drawText(mDataArea[i], areaWidth * i + 10 + getPaddingLeft(), getPaddingTop() + (mMeasuredHeight - getPaddingTop() - getPaddingBottom()) / 4 + mTextsize / 2, mTimePaint); } canvas.restore();Copy the code

We calculate the offset distance in the touch event. However, the calculation is to be careful about the offset out of bounds when sliding left or right

@Override public boolean onTouch(View v, MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: startX = event.getX(); break; ACTION_MOVE: // Scroll the entire control if (isScrollArea) {float stopX = event.getx (); mTempset = (stopX - startX); Float offset = -(limitOffset + getPaddingLeft() + getPaddingRight()); If (mTempset <= offset && mTempset <= 0) {mOffset = offset; mTempset = 0; } else if (mOffset + mTempset >= 0 && mTempset >= 0) { mTempset = 0; } } else { } break; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: if (isScrollArea) { mOffset += mTempset; mTempset = 0; break; default: break; } invalidate(); return true; }Copy the code

In order to lift the finger with inertial sliding, it is necessary to know the sliding acceleration of lifting the finger. Fortunately, Google API has provided a method to obtain the acceleration. It is necessary to use VelocityTracker in front of onTouch to pass the event time into // and calculate the acceleration if (null == mVelocityTracker) { mVelocityTracker = VelocityTracker.obtain(); / / finger speed change after the lift} mVelocityTracker.com puteCurrentVelocity (200); mVelocityTracker.addMovement(event); Get to acceleration and remove the front used in ACTION_UP VelocityTracker incoming events int xVelocity = (int) mVelocityTracker. GetXVelocity (); setxVelocity(xVelocity); mVelocityTracker.clear(); Inertial sliding is then implemented by adding variable values to the offset mOffset using ValueAnimation Theme ator

/** * @param xVelocity */ protected void setxVelocity(int xVelocity) {if (math.abs (xVelocity) < 20) {return; } if (mAnimatorRunning ! = null && mAnimatorRunning.isRunning()) { return; } mAnimatorRunning = ValueAnimator.ofInt(0, xVelocity / 20).setDuration(Math.abs(xVelocity / 10)); mAnimatorRunning.setInterpolator(new DecelerateInterpolator()); mAnimatorRunning.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { mOffset += (int) animation.getAnimatedValue(); mTempset = 0; // Prevent content from sliding out of the control float offset = -(limitOffset + getPaddingLeft() + getPaddingRight()); if (mOffset + mTempset <= offset) { mOffset = offset; } else if (mOffset + mTempset >= 0) { mOffset = 0; } invalidate(); }}); mAnimatorRunning.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { invalidate(); }}); mAnimatorRunning.start(); }Copy the code

Now we’re going to do select and drag and that’s all going to be done in the touch event, slide and click and we’re going to separate the upper part of the control and we’re going to do the slide and we’re going to do the lower part of the control and we’re going to do the click so again in ACTION_DOWN we’re going to differentiate between the slide area and the selected area, the selected area is going to be Rect and the Rect boundary is going to be based on the finger slide area Calculates the starting and ending points to draw. The full touch code:

@Override public boolean onTouch(View v, // Calculate acceleration if (null == mVelocityTracker) {mVelocityTracker = velocityTracker.obtain (); / / finger speed change after the lift} mVelocityTracker.com puteCurrentVelocity (200); mVelocityTracker.addMovement(event); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: startX = event.getX(); If (event.gety () < getPaddingTop() + (mMeasuredHeight-getPaddingTop () -getPaddingBottom ()))  / 2) { isScrollArea = true; } else { isScrollArea = false; } float currentPos = math.abs (mOffset) + event.getx (); If (currentPos > mrect.right - mbitmap.getwidth () / 2 && currentPos < mrect.right + mBitmap.getWidth() / 2) { isClickImg = true; } else { isClickImg = false; } if (currentPos > mrect.left && currentPos < mrect.right && mrect.width () == areaWidth) { isClickContent = true; } else { isClickContent = false; If (mTempStartOffset == 0) {mTempStartOffset = mStartClickOffset; if (mTempStartOffset = mStartClickOffset; } break; ACTION_MOVE: // Scroll the entire control if (isScrollArea) {float stopX = event.getx (); mTempset = (stopX - startX); // Prevent content from sliding out of the control float offset = -(limitOffset + getPaddingLeft() + getPaddingRight()); If (mTempset <= offset && mTempset <= 0) {mOffset = offset; mTempset = 0; } else if (mOffset + mTempset >= 0 && mTempset >= 0) { mTempset = 0; If (isClickImg) {float endClickOffset = math.abs (mOffset) + math.abs (event.getx ()); If (endClickOffset > mTempStartOffset) {// Slide mEndClickOffset = endClickOffset; mStartClickOffset = mTempStartOffset; If (endClickOffset >= mDataArea.length * areaWidth) {mEndClickOffset = mdataArea.length * areaWidth; } isDrawLeft = false; MEndClickOffset = mTempStartOffset; mEndClickOffset = mTempStartOffset; MStartClickOffset = endClickOffset; if (mStartClickOffset <= getPaddingLeft()) { mStartClickOffset = 0; } isDrawLeft = true; If (isClickContent) {mStartClickOffset = mTempStartOffset + (event.getX() - startX); If (mStartClickOffset <= 0) {mStartClickOffset = 0; If (mStartClickOffset > mdataArea. length * areaWidth - areaWidth) {mStartClickOffset = mDataArea.length * areaWidth - areaWidth; } mEndClickOffset = mStartClickOffset + areaWidth; } } } break; ACTION_UP: case MotionEvent.ACTION_CANCEL: if (isScrollArea) {// Slide offset calculation mOffset += mTempset; mTempset = 0; } else {if (isClickImg) {int currentPos2 = (int) (math.abs (mOffset) + math.abs (event.getx ())); If (currentPos2 > mTempStartOffset) {int v1 = (int) ((int) (currentPos2 / areaWidth) * areaWidth); mEndClickOffset = currentPos2 % areaWidth > (areaWidth / 2) ? v1 + areaWidth : v1; mStartClickOffset = mTempStartOffset; if (mEndClickOffset - mStartClickOffset < areaWidth) { mEndClickOffset = mStartClickOffset + areaWidth; } if (mEndClickOffset > mdataArea.length * areaWidth) {mEndClickOffset = mdataArea.length * areaWidth; MEndClickOffset = mTempStartOffset; mEndClickOffset = mTempStartOffset; Int v1 = (int) ((int) (currentPos2 / areaWidth) * areaWidth); mStartClickOffset = currentPos2 % areaWidth > (areaWidth / 2) ? v1 + areaWidth : v1; If (mEndClickOffset - mStartClickOffset < areaWidth) {mStartClickOffset = mEndClickOffset - areaWidth; } } } else { float startClickOffset = (int) ((Math.abs(mOffset) + Math.abs(event.getX())) / areaWidth) * areaWidth; float endClickOffset = startClickOffset + areaWidth; Float v1 = event.getx () -startx; float v1 = event.getx () -startx; If (math.abs (v1) < 30) {in (startClickOffset == mStartClickOffset && endClickOffset == mEndClickOffset) { mStartClickOffset = 0; mEndClickOffset = 0; } else { mStartClickOffset = startClickOffset; If (mStartClickOffset > mdataArea.length * areaWidth - areaWidth) {mStartClickOffset = mdataArea.length * areaWidth - areaWidth; } mEndClickOffset = mStartClickOffset + areaWidth; } } else if (isClickContent) { mStartClickOffset = startClickOffset; if (mStartClickOffset <= 0) { mStartClickOffset = 0; If (mStartClickOffset > mDataArea.length * areaWidth - areaWidth) {mStartClickOffset = mDataArea.length * areaWidth - areaWidth; } mEndClickOffset = mStartClickOffset + areaWidth; }}} / / slide for inertial acceleration do if (isScrollArea) {int xVelocity = (int) mVelocityTracker. GetXVelocity (); setxVelocity(xVelocity); mVelocityTracker.clear(); } Check the interval callback if (! isScrollArea && mOnSelectAreaLienter ! = null) { setOnlisenter(); } startX = 0; isScrollArea = false; mTempStartOffset = 0; isClickContent = false; isDrawLeft = false; break; default: break; } invalidate(); return true; }Copy the code

Complete code:Making the address