Android自定义控件之可平移、缩放、旋转图片控件

先上效果图

效果图.gif

源码

一、需求分析

单点拖动图片对图片进行平移操作。双手缩放图片大小和旋转图片到一定的角度。图片缩放的时候 不能大于最大的缩放因子和小于最小的缩放因子。大于最大缩放因子或者小于最小缩放因子需要对图像进行回弹。图片旋转的角度只能为90度的倍数,不满足90度要进行回弹。图片回弹要一个渐变的效果。

二、 思路分析

大体思路:首先,Android中提供了Matrix类可以对图像进行处理。其次,要显示一张图片最容易想到的就是ImageView。回弹要求渐变的过程,可以通过属性动画进行设置。所以大体的思路是:继承ImageView,重写onTouchEvent()方法,判断事件类型,在对应的事件使用Matrix对图像进行变换。
Matrix是一个已经封装好的矩阵,最重要的作用就是对坐标点进行变换。
举个栗子:
1.某个点(x0,y0,1)通过单位矩阵E映射得到的点还是(x0,y0,1)。


2.调用单位矩阵E的postTranslate(dx,dy)。单位矩阵转换成矩阵T

3.点(x0,y0,1)通过矩阵T映射得到的点就会做如下的变换


可以看到点(x0,y0,1)经过T矩阵在x轴方向上平移了dx,在y轴方向上平移了dy。

通过以上的变换可以得到具体的思路:我们维护一个图像对应的矩阵mCurrentMatrix,该矩阵主要是对ImageView中的图像的各个点进行映射。ImageView在容器位置摆放完成之后,置mCurrentMatrix矩阵为单位矩阵。当onTouchEvent()方法中触发单点触控并且手指进行平移的时候,调用矩阵mCurrentMatrix的postTranslate(dx,dy),对mCurrentMatrix进行变换。当手指抬起,利用变换结束后的矩阵对图像的各个点进行映射,从而得到平移变换后的图像。同理可得,在两只手指进行缩放旋转的时候,我们对矩阵mCurrentMatrix进行各种变换,当缩放旋转的事件结束再利用变换完的矩阵去映射图像的各个点,从而得到缩放、旋转后的图像。

三、Matrix

安卓自定义View进阶 - Matrix原理
安卓自定义View进阶 - Matrix详解

  • 对于Matrix的使用以上两篇博客中有详细的描述Matrix的使用方法以及原理。例如矩阵的第一行第三列的浮点数以及第二行第三列的浮点数是用来映射点的平移。

  • 注意调用Matrix的setXxx方法,会将Matrix之前的变换都清除。

四、主要方法

首先理清事件的逻辑:

  • 单只手指按下的时候是可以进行平移操作的,多点触控的时候不能进行平移操作。所以维护一个boolean变量mCanTranslate,在ACTION_DOWN的时候置位true,ACTION_POINTER_DOWN置为false。而平移操作的位移量需要手指滑动时与上一次点的坐标进行相比较,所以维护一个mLastSinglePoint记录手指按下以及手指滑动时上一次点的坐标。

  • 当两只手指按下的时候是可以进行旋转和缩放操作的,维护两个boolean变量:mCanRotate、mCanScale在ACTION_POINTER_DOWN置为true,ACTION_POINTER_UP置为false。

  • 图像的缩放比例因子采取的是前后两个手指间距离的比值。所以维护mLastDist,在ACTION_POINTER_DOWN的时候算出两只手指间的欧氏距离。在ACTION_MOVE的时候,算出新的手指间的距离,并和上一次手指间距离的运算求出比例,更新mLastDist为本次手指间的距离。

  • 图像的旋转角度采取的是两只手构成的向量转过的角度。维护一个mLastVector。在ACTION_POINTER_DOWN的时候记录两只手指构成的向量。在ACTION_MOVE的时候,记录新的两只手指构成的向量,求出转过的角度,最后更新mLastVector为本次两手指构成的向量。

初始化图像大小和位置

缩放图像大小和控件大小自适应,平移图像中心和控件中心重合

 private void init() {
        mCurrentMatrix.reset();
        upDateBoundRectF();
        float scaleFactor = Math.min(getWidth() / mBoundRectF.width(), getHeight() / mBoundRectF.height());
        mInitialScaleFactor = scaleFactor;
        mTotalScaleFactor *= scaleFactor;
        //以图片的中心点进行缩放,缩放图片大小和控件大小适应
        mCurrentMatrix.postScale(scaleFactor, scaleFactor, mBoundRectF.centerX(), mBoundRectF.centerY());
        //将图片中心点平移到和控件中心点重合
        mCurrentMatrix.postTranslate(getPivotX() - mBoundRectF.centerX(), getPivotY() - mBoundRectF.centerY());
        //对图片进行变换,并更新图片的边界矩形
        transform();
    }

onTouchEvent()函数

 /**
     * 当单点触控的时候可以进行平移操作
     * 当多点触控的时候:可以进行图片的缩放、旋转
     * ACTION_DOWN:标记能平移、不能旋转、不能缩放
     * ACTION_POINTER_DOWN:如果手指个数为2,标记不能平移、能旋转、能缩放
     * 记录平移开始时两手指的中点、两只手指形成的向量、两只手指间的距离
     * ACTION_MOVE:进行平移、旋转、缩放的操作。
     * ACTION_POINTER_UP:有一只手指抬起的时候,设置图片不能旋转、不能缩放,可以平移
     *
     * @param event 点击事件
     * @return
     */
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getActionMasked()) {
            //单点触控,设置图片可以平移、不能旋转和缩放
            case MotionEvent.ACTION_DOWN:
                mCanTranslate = true;
                mCanRotate = false;
                mCanScale = false;
                //记录单点触控的上一个单点的坐标
                mLastSinglePoint.set(event.getX(), event.getY());
                break;
            case MotionEvent.ACTION_POINTER_DOWN:
                animator.cancel();
                //多点触控,设置图片不能平移
                mCanTranslate = false;
                //当手指个数为两个的时候,设置图片能够旋转和缩放
                if (event.getPointerCount() == 2) {
                    mCanRotate = true;
                    mCanScale = true;
                    //记录两手指的中点
                    PointF pointF = midPoint(event);
                    //记录开始滑动前两手指中点的坐标
                    mLastMidPoint.set(pointF.x, pointF.y);
                    //记录开始滑动前两个手指之间的距离
                    mLastDist = distance(event);
                    //设置向量,以便于计算角度
                    mLastVector.set(event.getX(0) - event.getX(1), event.getY(0) - event.getY(1));
                }
                break;
            case MotionEvent.ACTION_MOVE:
                //判断能否平移操作
                if (mCanTranslate) {
                    float dx = event.getX() - mLastSinglePoint.x;
                    float dy = event.getY() - mLastSinglePoint.y;
                    //平移操作
                    translation(dx, dy);
                    //重置上一个单点的坐标
                    mLastSinglePoint.set(event.getX(), event.getY());
                }
                //判断能否缩放操作
                if (mCanScale) {
                    float scaleFactor = distance(event) / mLastDist;
                    scale(scaleFactor);
                    //重置mLastDist,让下次缩放在此基础上进行
                    mLastDist = distance(event);
                }
                //判断能否旋转操作
                if (mCanRotate) {
                    //当前两只手指构成的向量
                    PointF vector = new PointF(event.getX(0) - event.getX(1), event.getY(0) - event.getY(1));
                    //计算本次向量和上一次向量之间的夹角
                    float degree = calculateDeltaDegree(mLastVector, vector);
                    rotation(degree);
                    //更新mLastVector,以便下次旋转计算旋转过的角度
                    mLastVector.set(vector.x, vector.y);
                }
                //图像变换
                transform();
                break;
            case MotionEvent.ACTION_POINTER_UP:
                //当两只手指有一只抬起的时候,设置图片不能缩放和选择,能够进行平移
                if (event.getPointerCount() == 2) {
                    mCanScale = false;
                    mCanRotate = false;
                    mCanTranslate = true;
                    //重置旋转和缩放使用到的中点坐标
                    mLastMidPoint.set(0f, 0f);
                    //重置两只手指的距离
                    mLastDist = 0f;
                    //重置两只手指形成的向量
                    mLastVector.set(0f, 0f);
                }
                //获得开始动画之前的矩阵
                mCurrentMatrix.getValues(mBeginMatrixValues);
                //缩放回弹
                backScale();
                upDateBoundRectF();
                //旋转回弹
                backRotation();
                upDateBoundRectF();
                //获得动画结束之后的矩阵
                mCurrentMatrix.getValues(mEndMatrixValues);
                animator.start();
                break;
            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_UP:
                backTranslation();
                upDateBoundRectF();
                mLastSinglePoint.set(0f, 0f);
                mCanTranslate = false;
                mCanScale = false;
                mCanRotate = false;
                break;
        }
        return true;
    }

平移操作

将图像对应的矩阵进行变换。

  protected void translation(float dx, float dy) {
        //检查图片边界的平移是否超过控件的边界
        if (mBoundRectF.left + dx > getWidth() - 20 || mBoundRectF.right + dx < 20
                || mBoundRectF.top + dy > getHeight() - 20 || mBoundRectF.bottom + dy < 20) {
            return;
        }
        mCurrentMatrix.postTranslate(dx, dy);
    }

缩放操作

mBoundRectF为记录图像边界的矩形。缩放的时候选取图像的中心进行缩放。

  private void scale(float scaleFactor) {
        //累乘得到总的的缩放因子
        mTotalScaleFactor *= scaleFactor;
        mCurrentMatrix.postScale(scaleFactor, scaleFactor, mBoundRectF.centerX(), mBoundRectF.centerY());
    }

旋转操作

旋转的时候旋转的旋转中心也是图像的中心

    private void rotation(float degree) {
        //旋转变换
        mCurrentMatrix.postRotate(degree, mBoundRectF.centerX(), mBoundRectF.centerY());

    }

图像中各个点的映射

调用ImageView的setImageMatrix(Matrix matrix)会让ImageView根据设置的matrix去重新绘制图像。

   private void transform() {
        setImageMatrix(mCurrentMatrix);
        upDateBoundRectF();
    }

更新图像的矩形边界

获得图像的矩形,并根据矩阵映射矩形各个点的坐标。

 private void upDateBoundRectF() {
        if (getDrawable() != null) {
            mBoundRectF.set(getDrawable().getBounds());
            mCurrentMatrix.mapRect(mBoundRectF);
        }
    }

缩放回弹

    private void backScale() {
        float scaleFactor = 1.0f;
        //如果总的缩放比例因子比初始化的缩放因子还小,进行回弹
        if (mTotalScaleFactor / mInitialScaleFactor < mMinScaleFactor) {
            //1除以总的缩放因子再乘初始化的缩放因子,求得回弹的缩放因子
            scaleFactor = mInitialScaleFactor / mTotalScaleFactor * mMinScaleFactor;
            //更新总的缩放因子,以便下次在此缩放比例的基础上进行缩放
            mTotalScaleFactor = mInitialScaleFactor * mMinScaleFactor;
        }
        //如果总的缩放比例因子大于最大值,让图片放大到最大倍数
        else if (mTotalScaleFactor / mInitialScaleFactor > mMaxScaleFactor) {
            //求放大到最大倍数,需要的比例因子
            scaleFactor = mInitialScaleFactor / mTotalScaleFactor * mMaxScaleFactor;
            //更新总的缩放因子,以便下次在此缩放比例的基础上进行缩放
            mTotalScaleFactor = mInitialScaleFactor * mMaxScaleFactor;
        }
        mCurrentMatrix.postScale(scaleFactor, scaleFactor, mBoundRectF.centerX(), mBoundRectF.centerY());
    }

旋转回弹

private void backRotation() {
        //x轴方向的单位向量,在极坐标中,角度为0
        float[] x_vector = new float[]{1.0f, 0.0f};
        //映射向量
        mCurrentMatrix.mapVectors(x_vector);
        //计算x轴方向的单位向量转过的角度
        float totalDegree = (float) Math.toDegrees((float) Math.atan2(x_vector[1], x_vector[0]));
        float degree = totalDegree;
        degree = Math.abs(degree);
        //如果旋转角度的绝对值在45-135度之间,让其旋转角度为90度
        if (degree > 45 && degree <= 135) {
            degree = 90;
        } //如果旋转角度的绝对值在135-225之间,让其旋转角度为180度
        else if (degree > 135 && degree <= 225) {
            degree = 180;
        } //如果旋转角度的绝对值在225-315之间,让其旋转角度为270度
        else if (degree > 225 && degree <= 315) {
            degree = 270;
        }//如果旋转角度的绝对值在315-360之间,让其旋转角度为0度
        else {
            degree = 0;
        }
        degree = totalDegree < 0 ? -degree : degree;
        //degree-totalDegree计算达到90的倍数角,所需的差值
        mCurrentMatrix.postRotate(degree - totalDegree, mBoundRectF.centerX(), mBoundRectF.centerY());
    }

一些计算方法

    /**
     * 计算两个手指头之间的中心点的位置
     * x = (x1+x2)/2;
     * y = (y1+y2)/2;
     *
     * @param event 触摸事件
     * @return 返回中心点的坐标
     */
    private PointF midPoint(MotionEvent event) {
        float x = (event.getX(0) + event.getX(1)) / 2;
        float y = (event.getY(0) + event.getY(1)) / 2;
        return new PointF(x, y);
    }
    /**
     * 计算两个手指间的距离
     *
     * @param event 触摸事件
     * @return 放回两个手指之间的距离
     */
    private float distance(MotionEvent event) {
        float x = event.getX(0) - event.getX(1);
        float y = event.getY(0) - event.getY(1);
        return (float) Math.sqrt(x * x + y * y);//两点间距离公式
    }
    /**
     * 计算两个向量之间的夹角
     *
     * @param lastVector 上一次两只手指形成的向量
     * @param vector     本次两只手指形成的向量
     * @return 返回手指旋转过的角度
     */
    private float calculateDeltaDegree(PointF lastVector, PointF vector) {
        float lastDegree = (float) Math.atan2(lastVector.y, lastVector.x);
        float degree = (float) Math.atan2(vector.y, vector.x);
        float deltaDegree = degree - lastDegree;
        return (float) Math.toDegrees(deltaDegree);
    }

五、动画

要求图像的变换是一个渐变的过程,很容易想到的就是属性动画。因为属性动画本身就是对值进行不断set的过程。而我们维护的矩阵也是一个值,所以很自然可以想到,如果得到回弹之前的矩阵的值以及回弹之后矩阵的值,就可以根据动画监听器中动画当前的系数值去改变矩阵的值。

/**
     * 动画监听器
     */
    private ValueAnimator.AnimatorUpdateListener animatorUpdateListener = new ValueAnimator.AnimatorUpdateListener() {
        @Override
        public void onAnimationUpdate(ValueAnimator animation) {
            //获得动画过程当前的系数值
            float animatedValue = (float) animation.getAnimatedValue();
            for (int i = 0; i < 9; i++) {
                //使用渐变过程中的系数值去变换矩阵
                mTransformMatrixValues[i] = mBeginMatrixValues[i] + (mEndMatrixValues[i] - mBeginMatrixValues[i]) * animatedValue;
            }
            //动态更新矩阵中的值
            mCurrentMatrix.setValues(mTransformMatrixValues);
            //图像变化
            transform();
        }
    };

    /**
     * 动画监听器
     */
    private Animator.AnimatorListener animatorListener = new AnimatorListenerAdapter() {
        @Override
        public void onAnimationEnd(Animator animation) {
            mCurrentMatrix.setValues(mEndMatrixValues);
            transform();
        }
    };

对animator对象设置完监听器之后,就可以在手指抬起的时候调用属性动画的start()方法开启动画。

六、总结

自定义可平移、缩放、旋转的控件主要点有两个方面:一是onTouchEvent()中判断平移、旋转、缩放的触发条件,平移位移量、缩放比例因子、旋转角度的计算。二是Matrix矩阵的应用。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,236评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,867评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,715评论 0 340
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,899评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,895评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,733评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,085评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,722评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,025评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,696评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,816评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,447评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,057评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,009评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,254评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,204评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,561评论 2 343

推荐阅读更多精彩内容