An overview of the

In the previous article, we wrote about several custom views that support highlighting fixed lines and sliding text, with the following effects:

This article will write a more interesting custom View, I think everyone in the work should basically write similar ~ a support finger pan, zoom, rotation control:

Before introducing the implementation, let’s review the relevant knowledge.

MotionEvent

There are many types of events in MotionEvent, and the events that are relevant to this article are finger swiping on the screen, which are related to the following:

  • ACTION_DOWN
  • ACTION_MOVE
  • ACTION_UP

This finger swipe creates a sequence of events, called the event flow: ACTION_DOWN -> ACTION_MOVE -> ACTION_MOVE ->… ACTION_MOVE -> ACTION_UP.

Translation control is to receive these events, by calculating the offset, and then translation.

Look again at the two coordinate methods associated with MotionEvent:

  • getX/Y(): Gets the distance between the touch point and the left/top edge of its View. So if the control follows the finger, its getX/Y() method gets the coordinate value as the distance from its own boundary, unchanged.
  • getRawX/Y(): Gets the distance between the touch point and the left/top edge of the screen. The value obtained by this method changes if the control moves with the finger.

translation

There is no need to elaborate on the Coordinate system of Android, and it is estimated that everyone has seen those coordinate system diagrams on the Internet. Here are a few methods of View:

public final int getWidth(a) {
    return mRight - mLeft;
}

public final int getHeight(a) {
    return mBottom - mTop;
}

public float getX(a) {
    return mLeft + getTranslationX();
}

public float getY(a) {
    return mTop + getTranslationY();
}
Copy the code

The position and size of a View are determined by the left, top, right, and bottom parameters, which are relative to its parent View. The view.getx /Y() method represents the x, Y value of a View with the top left corner of its parent View as the origin, and setTranslationX/Y() is used to translate the View.

Here are a few ways to translate a View:

layout(), offsetLeftAndRight(), offsetTopAndBottom()

layout(left + dx, top + dy, right + dx, bottom + dy)

/ / equivalent to
offsetLeftAndRight(dx)
offsetTopAndBottom(dy)
Copy the code

This method directly changes the layout parameters of the View, such as left, top, right, and bottom.

setX(), setY(), setTranslationX(), setTranslationY()

public void setX(float x) {
    setTranslationX(x - mLeft);
}

public void setY(float y) {
    setTranslationY(y - mTop);
}
Copy the code

According to the relationship between x/y and translationX/ y, it can be known that the principle of the two approaches is the same. They all change the translation value of the View, but do not change the layout parameters left, right, top, bottom, etc.

Property animation actually changes the pan as well. So one of the differences between property animation and tween animation is that the tween animation changes the position of the View, but the event response area is still where it was before; In addition to changing the display position of the View, the property animation also changes its event response area. The reason why the display effect changes is that the Canvas matrix has been transformed, and the reason why it can still respond to events is that the corresponding mapping will be made when the events are distributed, so that the events can respond to the changed area.

scrollTo(), scrollBy()

These two methods, seen in the previous article, affect the content of the View, i.e. the displacement of the canvas, so it can be shifted like this:

(parent as? View)? .scrollBy(-dx, -dy)Copy the code

It also corresponds to changing the area of the event response.

rotating

Rotations use the view.setrotation () method. Just like translationX, it does not change the layout parameters left, right, top, bottom, etc. Note that the rotated view.getLocationOnScreen () and so on will return the View’s position coordinates as they were in the upper left corner of the View.

Associated with the pivot is the pivot argument setPivotX/Y(), which represents the pivot point of the rotation, which is the View center by default.

The zoom

Scaling uses the view.setScalex /Y() method, which also doesn’t change layout parameters left, right, top, bottom, etc., so even if the View looks bigger or smaller after scaling, But the length and width returned by its getWidth() and getHeight() methods remain the same, while the event response area changes because the event distribution is mapped.

The center point is also set through setPivotX/Y().

Custom View implementation

The effect can be seen in the figure above, with the code at the end of the article. This control has two forms, one is a large panel, a small panel after folding.

  • Panels expand and collapse using zooming
  • The middle of the top of the large panel can be touched and translated. After folding, the small panel can also be moved and automatically adsorbed to the boundary
  • The Activity holds the portrait, but the control can sense the portrait, using the rotation of the View

Finger translation

Finger panning requires overriding the onTouchEvent method to calculate how far the finger moves and move the control. Here, we need to determine whether the move button of the large panel or the small panel is touched according to the coordinates of the Down event. Only these two views can respond to the pan event.

override fun onTouchEvent(event: MotionEvent?).: Boolean {
    // If the control is touched but not dragged, it returns directly
    if (event == null|| (event.action == MotionEvent.ACTION_DOWN && ! touchInTransView(event) && ! touchInLittlePanel(event))) {// If a large panel is displayed, the touch event is blocked and the control below is not given a response
        return inBigPanel()
    }
    if (isBigDrag || touchInTransView(event)) {
        // Handle large panel drag
        return onBigPanelTouchEvent(event)
    }
    if (isLittleDrag || touchInLittlePanel(event)) {
        // Handle small panel drag
        return onLittlePanelTouchEvent(event)
    }
    return inBigPanel()
}
Copy the code

Next, take the large panel pan as an example to see the implementation code:

private fun onBigPanelTouchEvent(event: MotionEvent): Boolean {
    val x = event.x
    val y = event.y
    when (event.action) {
        MotionEvent.ACTION_DOWN -> {
            // Record the coordinates of down
            lastX = x
            lastY = y
            isBigDrag = true
        }
        MotionEvent.ACTION_MOVE -> {
            // Calculate the translation distance
            val dx = x - lastX
            val dy = y - lastY
            // Handle the calculation of translation in horizontal and vertical screens
            when {
                isPortrait() -> {
                    tranX += dx
                    tranY += dy
                }
                isLandLeft() -> {
                    tranX -= dy
                    tranY += dx
                }
                else -> {
                    tranX += dy
                    tranY -= dx
                }
            }
            // Correct the boundary and translate it
            correctBigBorder()
        }
        MotionEvent.ACTION_UP -> {
            isBigDrag = false}}return true
}
Copy the code

Note that the motionEvent.getx /Y() method was used. You can also use the motionEvent.getrawx /Y() method, but you need to update the lastX/Y value every time you move. CorrectBigBorder () : correctBigBorder()

private fun correctBigBorder(a) {
    val lMax = leftMax()
    if (tranX < lMax) {
        tranX = lMax
    }
    val rMax = rightMax()
    if (tranX > rMax) {
        tranX = rMax
    }
    val tMax = topMax()
    if (tranY < tMax) {
        tranY = tMax
    }
    val bMax = bottomMax()
    if (tranY > bMax) {
        tranY = bMax
    }
    // Set the Translation property of the View, here is the code for the real translation
    translationX = tranX
    translationY = tranY
}
Copy the code

In the above case, we need to judge whether the move event exceeds the boundary, then correct it, and then carry out the translation of the View.

/** * The left edge of the large panel slide * [getTranslationX] = [getX] - [getLeft
private fun leftMax(a) = if (isPortrait()) marginL - left else (height() - width()) / 2 + marginL - left

/** * The top edge of the large panel slide * [getTranslationY] = [getY] - [getTop] */
private fun topMax(a) = if (isPortrait()) marginT - top else marginT - top - (height() - width()) / 2

/** * The right edge of the large panel slide * [getTranslationX] = [getX] - [getLeft] */
private fun rightMax(a) = maxWidth() - orientedWidth() + leftMax()

/** * The bottom edge of the large panel slide * [getTranslationY] = [getY] - [getTop] */
private fun bottomMax(real: Boolean = true) = maxHeight() - orientedHeight() + topMax()

private fun width(a) = parentWidth * 0.6 f

private fun height(a) = parentHeight * 0.4 f

/** * The width of the panel after separating the vertical and horizontal directions */
private fun orientedWidth(a) = if (isPortrait()) width() else height()

/** * The height of the panel after separating the vertical and horizontal directions */
private fun orientedHeight(a) = if (isPortrait()) height() else width()

/** * The width of the display area */
private fun maxWidth(a) = parentWidth - marginL - marginR

/** * The height of the display area */
private fun maxHeight(a) = parentHeight - marginT - marginB
Copy the code

Horizontal and vertical screen induction (rotation)

Since the Activity is fixed portrait, the control needs to sense the portrait itself and then rotate:

private val orientationListener: OrientationEventListener by lazy {
    object : OrientationEventListener(context, SensorManager.SENSOR_DELAY_NORMAL) {
        override fun onOrientationChanged(orientation: Int) {
            val cur = if (orientation >= 330 || orientation < 30) {
                // vertical down
                Surface.ROTATION_0
            } else if (orientation in 60.119.) {
                // Landscape to the right
                Surface.ROTATION_90
            } else if (orientation in 150.209.) {
                // Portrait up
                Surface.ROTATION_180
            } else if (orientation in 240.299.) {
                // landscape to left
                Surface.ROTATION_270
            } else {
                Surface.ROTATION_0
            }
            // No panel is being expanded or folded && The large panel is being expanded && the screen rotates
            if(! isFolding && inBigPanel() &&this@FancyPanel.orientation ! = cur) {this@FancyPanel.orientation = cur
                // Set the rotation center to the control center point
                pivotX = width() / 2f
                pivotY = height() / 2f
                // Rotate different angles
                rotation = when {
                    isPortrait() -> 0f
                    isLandLeft() -> 90f
                    else -> 270f
                }
                // Display the button to retract the panel only in portrait
                if (isPortrait()) {
                    foldView.show()
                } else {
                    foldView.hide()
                }
                // Correct boundaries to prevent sliding out of the screen after rotation
                correctBigBorder()
            }
        }
    }
}
Copy the code

The rotation is done by setting the Rotate property. In small panel mode, vertical and horizontal rotation is not performed. If you want to support it, you need to set the pivot point pivot value and recalculate the corresponding transX and transY values, as this will affect the calculation of the translation boundary.

Expand and collapse panel (zoom)

Panel expansion and collapse are achieved by scaling the View, for example:

// Fold up the panel
private fun fold(a) {
    if (isFolding) {
        return
    }
    isFolding = true
    // Correct the center of the zoom
    correctPivot(isBigPanelInLeft())
    val xScaleAnim = ObjectAnimator.ofFloat(bigPanel, "scaleX", littlePanelSize / width())
    val yScaleAnim = ObjectAnimator.ofFloat(bigPanel, "scaleY", littlePanelSize / height())
    val animSet = AnimatorSet()
    animSet.playTogether(xScaleAnim, yScaleAnim)
    animSet.duration = 500
    animSet.addListener(object : AnimatorListenerAdapter() {
        override fun onAnimationEnd(animation: Animator?). {
            // After folding the panel, attach the small panel to the left/right edge
            adjustPanelAttach(isBigPanelInLeft())
            littlePanel.show()
            isFolding = false
        }
    })
    animSet.start()
}

/** * Corrects zooming in the center of the animation when expanding and collapsing large panels */
private fun correctPivot(inLeft: Boolean) {
    if (inLeft) {
        bigPanel.pivotX = 0f
        bigPanel.pivotY = 0f
    } else {
        bigPanel.pivotX = width()
        bigPanel.pivotY = 0f}}/** * ADAPTS small panel offset in large panel */
private fun adjustPanelAttach(inLeft: Boolean) {
    if (inLeft) {
        littlePanel.x = 0f
        littlePanel.y = 0f
        x = littlePanelPadding
    } else {
        littlePanel.x = width() - littlePanelSize
        littlePanel.y = 0f
        x = parentWidth - littlePanelPadding - width()
    }
    tranX = x - left
}
Copy the code

There are two main points to note:

  • CorrectPivot: correctPivot the center of the zoom animation when the large panel is expanded and retracted. The zoom center is set to the left or right of the View depending on whether the panel is near the left or right side of the screen.
  • AdjustPanelAttach: After making it a small board, adjustPanelAttach the board to the left and right sides to show.

conclusion

For translation, rotation and zooming, we should encounter in the usual development, this article through this control to learn about the relevant operation, the code has been uploaded to Github, there is a better way to achieve students welcome suggestions ~

If the content of the article is wrong, welcome to point out, common progress! I think it’s good. Wait for a “like” before I leave

Link to blog