Android相关知识

Android View的移动、缩放、旋转组合效果实战

2020-03-24  本文已影响0人  silladus

首先确定功能效果


P0LROO8LUJ7S.png

1.一个View由关闭按钮、文字、拖拽按钮组成
2.拖拽文字部分移动这个View
3.在拖拽按钮处拖拽时可放大、旋转该View

该View直接通过布局实现

<?xml version="1.0" encoding="utf-8"?>
<com.silladus.subtitles.SubtitlesEditView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/subtitlesEditView"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content">

    <FrameLayout
        android:id="@+id/v_bg"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:layout_margin="8dp"
        android:background="@drawable/shape_bg_subtitles_edit_text"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/tv_content"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="8dp"
        android:gravity="center"
        android:paddingStart="14dp"
        android:paddingTop="6dp"
        android:paddingEnd="14dp"
        android:paddingBottom="6dp"
        android:text="测试文本"
        android:textColor="#ffffffff"
        android:textSize="28sp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <ImageView
        android:id="@+id/iv_close"
        android:layout_width="18dp"
        android:layout_height="18dp"
        android:src="@drawable/ic_close_subtitles_edit"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <ImageView
        android:id="@+id/iv_pull"
        android:layout_width="18dp"
        android:layout_height="18dp"
        android:src="@drawable/ic_pull_subtitles"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent" />

</com.silladus.subtitles.SubtitlesEditView>

这个View就是SubtitlesEditView,为了有效控制内部View的位置关系和观察预览效果,直接继承自ConstraintLayout

public class SubtitlesEditView extends ConstraintLayout {
...
}

他们的位置关系如图:


QQ图片20200323205407.png

要处理平移、缩放、旋转直接在View的内部重写onTouchEvent处理事件比较好。
这里有功能细节:

1.点击关闭按钮要可以隐藏或移除View,
2.拖动拖拽按钮可以拉伸旋转View
3.文字部分可通过拖拽移动View,点击响应点击事件

如果统一在View内部由onTouchEvent处理事件则要判断事件触发的位置是否在相应的区域,同时做对应的逻辑,有点繁琐,不如对应的功能交由他们自己处理。
所以直接对关闭按钮closeView设置点击监听,直接处理点击事件,closeView的事件会在这里消耗,不会回传到SubtitlesEditView上,在这一块区域SubtitlesEditView里重写的onTouchEvent无需做判断。
而拖拽按钮pullView要处理View的缩放和旋转,要处理比较复杂的事件逻辑,直接设置setOnTouchListener进行事件监听,相当于在pullView内部重写onTouchEvent。事件也不会继续往其父布局SubtitlesEditView传递,剩下的移动和点击功能交由SubtitlesEditView处理就好了。
接下来就是具体的事件逻辑处理实现。
在实现之前先要了解事件触发点坐标的概念

1.触发点在屏幕中的坐标位置: event.getRowX()、event.getRowY()
2.触发点在ViewGroup容器中的坐标位置:event.getX()、event.getY()

他们的意义如图所示:


QQ图片20200323213508.png

1.先在View中处理View的移动

View位置的变化通过计算事件触发点x、y坐标变化量求得,在手指按下时记录事件触发的位置作为参照坐标原点。

    private float mOriginalX;
    private float mOriginalY;
    private float mOriginalRawX;
    private float mOriginalRawY;

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mOriginalX = getX();
                mOriginalY = getY();
                mOriginalRawX = event.getRawX();
                mOriginalRawY = event.getRawY();
                break;
            case MotionEvent.ACTION_MOVE:
                break;
            case MotionEvent.ACTION_UP:
                break;
        }

        return true;
    }

在手指按住屏幕移动的时候更新View的位置实现手指拖着View移动的效果。
mOriginalX,mOriginalY 即是View移动前的坐标,event.getRawX() - mOriginalRawX,event.getRawY() - mOriginalRawY即是x,y坐标的变化量

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mOriginalX = getX();
                mOriginalY = getY();
                mOriginalRawX = event.getRawX();
                mOriginalRawY = event.getRawY();
                break;
            case MotionEvent.ACTION_MOVE:
                setX(mOriginalX + event.getRawX() - mOriginalRawX);
                setY(mOriginalY + event.getRawY() - mOriginalRawY);
                break;
            case MotionEvent.ACTION_UP:
                break;
        }

        return true;
    }

2.处理View文字的点击

要先判断事件触发的位置是否在文字区域再决定是否响应点击事件
View中有个getGlobalVisibleRect方法可以获取View在屏幕中的可见部分区域Rect ,通过判断事件位置是否在该区域确定是否响应事件。

    public static boolean isInRect(float x, float y, Rect rect) {
        return x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom;
    }

当我们手机从屏幕上离开时处理点击事件。

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mOriginalX = getX();
                mOriginalY = getY();
                mOriginalRawX = event.getRawX();
                mOriginalRawY = event.getRawY();
                break;
            case MotionEvent.ACTION_MOVE:
                setX(mOriginalX + event.getRawX() - mOriginalRawX);
                setY(mOriginalY + event.getRawY() - mOriginalRawY);
                break;
            case MotionEvent.ACTION_UP:
                Rect notCancelRect = new Rect();
                tvContent.getGlobalVisibleRect(notCancelRect);
                if (event.getEventTime() - event.getDownTime() < 200 
                        && Math.abs(event.getRawX() - mOriginalRawX) < 10 
                        && Math.abs(event.getRawY() - mOriginalRawY) < 10
                        && isInRect(event.getRawX(), event.getRawY(), notCancelRect)) {
                    if (onContentClickListener != null) {
                        onContentClickListener.onClick(this, tvContent);
                    }
                }
                break;
        }

        return true;
    }

在这里我不仅判断事件是否在该区域上,还加入从手指按下到离开在200毫秒内,x、y坐标偏移小于10才算触发的限制。

3.处理View的旋转

View的旋转拉伸以View的中心点为基本点最好处理,我们通过View.getX(),View.getY()可以得到在ViewGroup中View最左上位置的坐标,中心点坐标即:

View.getX() + View.getWidth() * 0.5f,View.getY() + View.getHeight() * 0.5f

View的拉伸变化量需要计算事件点位置到中心点位置位移的变化量。
View的旋转变化角度需要计算事件点位置到中心点位置位移变化角度。

因为这里我们事件处理是分开的,event.getX,event.getY不好处理整体坐标关系,所以取相对屏幕的坐标event.getRowX,event.getRowY,View的中点也是计算在屏幕中的坐标。

对pullView设置事件监听

        v.setOnTouchListener((v1, event) -> {
            switch (event.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    break;
                case MotionEvent.ACTION_MOVE:
                    break;
                case MotionEvent.ACTION_UP:
                    break;
            }
            return true;
        });

手指按下pullView时记录事件在屏幕中的触发位置oldRawX,oldRawY ,取得SubtitlesEditView的中点在屏幕位置的坐标bgOX,bgOY 。中点的坐标可以借助pullView和closeView的坐标获得。
pullView和closeView的坐标在SubtitlesEditView旋转时是会变的,如何才能知道坐标?
View中有个方法getLocationOnScreen可以获得View在屏幕中的坐标。

int[] pullPosition = new int[2];
pullView.getLocationOnScreen(pullPosition);
int x = pullPosition[0];
int y = pullPosition[1];

通过getLocationOnScreen获得的xy坐标分别存在int数组的0、1位置。
closeView、pullView和中点正好在同一条直线上,取得closeView和pullView坐标求他们的坐标差的一半加上一个已知的等距坐标就可以得到中点坐标。


QQ图片20200323235416.png
计算旋转角度
QQ图片20200323235907.png

如图,计算旋转前记录的坐标点 (oldRawX,oldRawY)和手指旋转操作移动的坐标点到中点坐标的向量,通过向量夹角求得角a的弧度再转成角度

    private float angleBetweenLines(float fX, float fY, float nfX, float nfY, float cfX, float cfY) {
        float angle1 = (float) Math.atan2((fY - cfY), (fX - cfX));
        float angle2 = (float) Math.atan2((nfY - cfY), (nfX - cfX));
        float angle = ((float) Math.toDegrees(angle2 - angle1)) % 360;
        return angle;
    }

设置旋转

   // 旋转
  setRotation(angleBetweenLines(oldRawX, oldRawY, event.getRawX(), event.getRawY(), bgOX, bgOY) + defaultAngle);

这里的defaultAngle是记录旋转角度的值,View每一次的旋转都是在上一次旋转的基础上完成。

计算缩放量

通过手指在屏幕上的移动坐标与View的中点坐标求得手指移动事件触发点相对中点的位移求得两点的距离,

    /**
     * 计算两点之间的距离
     *
     * @return 两点之间的距离
     */
    private float spacing(float xs, float ys) {
        return (float) Math.sqrt(xs * xs + ys * ys);
    }

从而求得缩放系数

                    float newDist = spacing(event.getRawX() - bgOX, event.getRawY() - bgOY);
                    scale = newDist / oldDist;

完整的缩放旋转代码:

    private float oldRawX;
    private float oldRawY;

    private float oldSize;

    private float px, py;
    private float pw, ph;

    private float bgOX;
    private float bgOY;

    ...

        v.setOnTouchListener((v1, event) -> {
            switch (event.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    oldRawX = event.getRawX();
                    oldRawY = event.getRawY();
                    oldSize = tvContent.getTextSize();

                    px = getX();
                    py = getY();
                    pw = getWidth();
                    ph = getHeight();

                    defaultAngle = getRotation();

                    oldDist = spacing(oldRawX - getPivotX(), oldRawY - getY() - getHeight() * 0.5f);

                    int[] bgPosition = new int[2];
                    bgView.getLocationOnScreen(bgPosition);

                    int[] closePosition = new int[2];
                    closeView.getLocationOnScreen(closePosition);

                    int[] pullPosition = new int[2];
                    pullView.getLocationOnScreen(pullPosition);

                    float dx = Math.abs(closePosition[0] - pullPosition[0]) * 0.5f;
                    float dy = Math.abs(closePosition[1] - pullPosition[1]) * 0.5f;
                    if (bgPosition[0] < pullPosition[0]) {
                        bgOX = bgPosition[0] + dx;
                    } else {
                        bgOX = bgPosition[0] - dx;
                    }
                    if (bgPosition[1] < pullPosition[1]) {
                        bgOY = bgPosition[1] + dy;
                    } else {
                        bgOY = bgPosition[1] - dy;
                    }
                    break;
                case MotionEvent.ACTION_MOVE:

                    // 旋转
                    setRotation(angleBetweenLines(oldRawX, oldRawY, event.getRawX(), event.getRawY(), bgOX, bgOY) + defaultAngle);

                    // 拉伸
                    setX(px + (pw - getWidth()) * 0.5f);
                    setY(py + (ph - getHeight()) * 0.5f);
                    float newDist = spacing(event.getRawX() - bgOX, event.getRawY() - bgOY);
                    scale = newDist / oldDist;
                    if (newDist > oldDist + 1) {
                        zoom(scale);
                        oldDist = newDist;
                    }
                    if (newDist < oldDist - 1) {
                        zoom(scale);
                        oldDist = newDist;
                    }
//                    float newSize = event.getRawX() - oldRawX + oldSize;
//                    tvContent.setTextSize(TypedValue.COMPLEX_UNIT_PX, newSize);


                    break;
                case MotionEvent.ACTION_UP:
                    break;
            }
            return true;
        });

实现效果:


small.gif

如有帮助,求赐我一个Star
源码地址:https://github.com/silladus/SubtitlesView

上一篇下一篇

猜你喜欢

热点阅读