一步步自定义一个封面选择框


layout: post
title: 一步步自定义一个封面选择框
tags:

  • Android开发
  • 自定义View
  • 16:9
  • 滑动事件
    categories:
  • Android开发
    abbrlink: 45123
    date: 2018-05-28 21:59:04

*本篇文章已授权微信公众号 guolin_blog (郭霖)独家发布

引言

很多时候我们拍摄视频用户是竖屏拍摄,但是一个视频的封面需要一个16:9的图片,并且允许用户自己选择,于是做了一个简单的自定义View,进行展示封面选择。

  • 先看看引入到项目的效果:
    image

自定义View的准备

  • 首先来说自定义View就是进行绘制,绘制肯定会需要确定大小,位置以及绘制的内容。对应的既是
    onMeasure()、onLayout()和onDraw()
    来看一张自定义View的流程图,对照图进行编写,变会轻松很多。
    image
  • 本次主要是在滑动监听上做功夫进行绘制,即onTouchEvent()

分析

1.当看到这自定义View的时候,我也是一脸懵逼。然后慢慢的思考。先绘制一个东西上去,让它跟着手指动起来就好,于是有了这样的代码

    public boolean onTouchEvent(MotionEvent event) {  
        switch (event.getAction()) {  
            case MotionEvent.ACTION_DOWN:  
                moveX = event.getX();  
                moveY = event.getY();  
                break;  
            case MotionEvent.ACTION_MOVE:  
                setTranslationX(getX() + (event.getX() - moveX));  
                setTranslationY(getY() + (event.getY() - moveY));  
                break;  
            case MotionEvent.ACTION_UP:  
                break;  
            case MotionEvent.ACTION_CANCEL:  
                break;  
        }  
  
        return true;  
    } 

2.动起来之后,我们继续再看目标,可以发现,我们只需要在Y轴上移动,并且是在一个长矩形中间滑动一个小的矩形。于是就绘制一个长矩形,和一个小矩形,修改onTouchEvent()中的方法,删除和x轴相关的代码。绘制矩形使用drawRect函数,传入两个点的坐标(左上角和右下角)和画笔。

canvas.drawRect(mC1X1, mC1Y1, mC1X2, mC1Y2, mChildPaint);

3.这一步也不算很难。继续绘制,发现背景需要绘制一张图片,这个时候需要在调用这个View的地方传递一张Bitmap过来。于是有了这样的代码:

    /**
     * 设置图片
     */
    public void setData(Bitmap bitmap) {
        if (bitmap == null) {
            throw new RuntimeException("bitmap can't null");
        }
        mParentBg = bitmap;
        invalidate();
    }
  • invalidate()进行重新绘制,调用之后,流程进行onDraw(),调用绘制bitmap的函数
        // 指定图片绘制区域
        Rect src = new Rect(0, 0, mParentBg.getWidth(), mParentBg.getHeight());
        // 指定图片在屏幕上显示的区域
        Rect dst = new Rect(mPX1, mPY1, mPX2, mPY2);
        canvas.drawBitmap(mParentBg, src, dst, null);
  • 先简单说明一下,mParentBg即为Bitmap对象,先获取要绘制的背景图片的大小,这里当然是把整个背景图绘制进行,然后显示的位置,即为矩形的位置,依然是左上角和右下角。我们不需要给图片着色,所以paint传null即可。
  1. 完成这几部之后,觉得很不错,大功告成,这篇文章到此为止了。问题来了,本想中间的选择矩形绘制为透明的,长矩形即父控件矩形绘制一个半透明的。但是发现,根本没有作用,绘制透明的就好像没有绘制一样。
  2. 于是,又开始认真思考。中间选择部分是透明的,也就是相当于没有绘制。那么就把父控件分为两个变化的子控件矩形,根据中间选择区域的变化,调整上下两个子控件矩形的大小。

编码

1.初始化。根据上面的分析之后,开始编码。首先是要定义一些初始化的东西,于是在构造函数中调用init:

    private void init(Context context) {

        mContext = context;

        mChildPaint = new Paint();
        mChildPaint.setColor(context.getResources().getColor(R.color.colorT));
        mChildPaint.setStyle(Paint.Style.FILL);
    }
  • 定义了绘制两个子控件的画笔,设置了抗锯齿和半透明带黑色蒙层的颜色

2.大小确定。根据流程肯定是测量出自定义View的大小:

    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        mParentHeight = MeasureSpec.getSize(heightMeasureSpec);
        mScreenWidth = MeasureSpec.getSize(widthMeasureSpec);
        //父控件宽度 16:9的宽
        mParentWidth = mParentHeight * 9 / 16f;
        //选中区域高度
        mChildHeight = mParentWidth * 9 / 16f;
        initCalc();
    }
  • 我们首先获取到了view的宽高,然后去进行父控件的宽高和子控件的宽高
  • 首先来说,父控件的高度肯定就是整个view的高度,而宽度的话,因为要求是16:9,所以根据高度计算出宽度
  • 有了父控件的宽度,也便有了中间选择矩形的宽度,也是要求16:9,所以根据比例计算出来高度大小
  • 获取到一些宽高之后,便马上进行对坐标点的计算。首先来看一张图

image

  • 这是我优化之前的一张草图。首先来说,绘制矩形只需要知道左上角和右下角的坐标。
  • 如图,第一个状态,是子控件1是0,也就是左上角即为父控件的左上角,右下角即为父控件的左上角,而这个状态中,子控件2呈现最大的状态,左上角的仅仅为父控件的减去一个选择区域的大小,右下角即为父控件的右下角。
  • 中间的状态进行变化,子控件1的左上角和子控件2的右下角始终和父控件一样,不进行变化。而这里只有上下滑动,所以,变化的仅仅为Y轴上面。根据Android的坐标系来说,Y轴向下为正方向。也就是子控件1的右下角的Y坐标和子控件2的左上角的Y坐标进行加减手指滑动的距离,然后进行重绘,即可达到绘制效果。
  • 第三个状态便是当滑到最底部的时候,原理和第一个状态类似,子控件1达到最大,左下角的坐标仅仅减去选择区域的高度;子控件2为0,左上角为父控件的左下角,右下角为父控件的右下角。
  • 搞清楚这些之后,开始计算初始化的坐标点:
    /**
     * 坐标点的计算
     * X轴基本不变,变化的是Y轴
     */
    private void initCalc() {
        //计算父控件的位置点
        mPX1 = (int) (mScreenWidth / 2f - mParentWidth / 2f);
        mPY1 = 0;
        mPX2 = (int) (mScreenWidth / 2f + mParentWidth / 2f);
        mPY2 = (int) (mParentHeight);

        //刚开始子控件1的位置点
        mC1X1 = mPX1;
        mC1Y1 = mPY1;
        mC1X2 = mPX2;
        mC1Y2 = mPY1;

        //刚开始子控件2的位置点
        mC2X1 = mPX1;
        mC2Y1 = (int) (mChildHeight);
        mC2X2 = mPX2;
        mC2Y2 = mPY2;
    }
  • 值得注意的是,我们需要把控件摆放到屏幕中间,所以,左上角的X便是屏幕宽度除以2减去计算出来的父控件宽度除以2。右下角同理是加上父控件宽度除以2。

3.位置确定。坐标计算完毕,进行设置view的大小:

    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        setMeasuredDimension((int) mParentWidth, (int) mParentHeight);
    }
  • 这里调用setMeasuredDimension()将我们计算好的宽高设置上去

5.绘制。因为我们这边已经通过坐标来进行了位置的确定,所以直接调用onDraw()进行绘制:

    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        //绘制父控件

        // 指定图片绘制区域
        Rect src = new Rect(0, 0, mParentBg.getWidth(), mParentBg.getHeight());
        // 指定图片在屏幕上显示的区域
        Rect dst = new Rect(mPX1, mPY1, mPX2, mPY2);
        canvas.drawBitmap(mParentBg, src, dst, null);

        //绘制子控件1
        canvas.drawRect(mC1X1, mC1Y1, mC1X2, mC1Y2, mChildPaint);


        //绘制子控件2
        canvas.drawRect(mC2X1, mC2Y1, mC2X2, mC2Y2, mChildPaint);
    }
  • 这里的基本都在分析的时候已经进行了说明,没有太多要说的地方。mParentBg即为传入Bitmap对象。

6.划动事件。要进行划动了,心里开始莫名的紧张,这部分是最不好进行控制。还是先看图:
[外链图片转存失败(img-QNTt3tAT-1562311790517)(http://ogrop3bok.bkt.clouddn.com/dragView2.png)]

  • MotionEvent.ACTION_DOWN:
    • 首先我们肯定是有一个拖拽范围的。因为我们只能拖选择框的地方才能有效。所以,图上右边紫色我写了可选中范围。
      • 在X轴上面,没的说,很好办,就在父控件的宽度内。
      • 但是在Y轴上面的话,需要动态根据选择框的位置进行变化了。
      • 一开始,我进行判断划动的时候,写死了区域。不在这个区域直接不进行划动监听。这样也是可以做到,但是效果并不好。第一点,当手指从不可选中区域划入到可选中的时候,这样会响应事件,表现出来的便是选择区域突然跳到手指最开始落下的地方(从上往下划动);第二点,从可选中区域划动到不可选中区域,这样不会响应时间,效果表现出来好像划不动一样。
//手指按下
//记录按下的距离
float beginY = event.getY();
if (beginY < mC1Y2) {
    //起始点在选择框上部,不做反应
    return false;
} else if (beginY > mC1Y2 + mChildHeight) {
    //起始点在选择框下部,不做反应
    return false;
}
- 我们记录手指按下的Y坐标,进行判断,如果小于了子控件1的右下角Y坐标,说明按下的时候在选择框的上面,那么不做反应,针对上述第一点。如果按下距离在子控件2的右下角加上一个可选取与的高度,那么说明按下的点再选择框的下部,那已经超出了可选范围,也不做反应。
  • 说完了可拖拽区域,现在来看一下划动的距离变化。其实这部分在分析的时候也说了,主要是就是加减手指移动的距离便可。
    • 要计算距离,这里需要减去一个按下的距离和可选区域的上边框的差值,否则,选择框会跳一下,然后以上边框为基准线进行改变,这显然不是我们想要的结果:
//记录按下的位置和选择区域的上边距的差
mDistanceY = beginY - mC1Y2;
  • MotionEvent.ACTION_MOVE:
    • 我们手指移动的距离为event.getY() - beginY。而我们实际要计算的是画在图中右边一点的实际移动的改变值,便是event.getY() - mDistanceY。event.getY()是指距离父控件的上边距,减去之前算好的mDistanceY,便可以比较精确得出实际移动的距离。可能还是会有疑问,为什么这样计算出来的距离会多一部分手指按下的距离和可选区域上边距的距离。因为我们这边是改变的子控件1和2的大小,子控件1是以右下角的Y,也可以理解为可选区域上边这根线绘制。如果不算手指和可选区域上边距的距离,那么效果就是划动起来,可选区域就会跳一下,然后以可选区域上边这根线在划动,而手指按下的时候明明和上边是有一定距离的。这部分可以尝试去掉进行感受。(感觉文字功底不好,有点扯不清,逃~)简单来说,反正就是要记录下手指按下距离可选区域上边的距离,在移动完之后,手指还是要距离可选区域同样的距离。嗯,就是这样,喵。
    • 这样就完工了。等等,不急。我们的可选区域肯定是不能划出父控件哒。也就是说可选区域上面不能划出父控件的上面,下面也不能划出父控件的下面的。
      • 往上划动。需要判断下子控件1右下角的Y不能小于0,即不能小于了父控件的Y,超过了则保持状态一。
      • 往下划动。需要判断下子控件1右下角的Y不能大于父控件高度减去一个可选区域的高度,超过了则保持状态三。
//往上滑动
if (mC1Y2 < 0) {
    //防止顶部超过出
    //子控件1为0
    mC1Y2 = 0;
    //子控件2为最大
    mC2Y1 = (int) (mSelectHeight);
} else if (mC1Y2 > mParentHeight - mSelectHeight) {
    //防止底部超过
    //子控件1为最大
    mC1Y2 = (int) (mParentHeight - mSelectHeight);
    //子控件2为0
    mC2Y1 = (int) (mParentHeight);
  • 最后执行一下重绘。invalidate()
  • 最后来一个完整的onTouchEvent()的代码,其实上诉已经说完了
    public boolean onTouchEvent(MotionEvent event) {
        //有效触控范围(X轴,Y轴另外判断)
        if (mC1X1 <= event.getRawX() && event.getRawX() <= mC1X1 + mParentWidth) {
            switch (event.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    //手指按下
                    //记录按下的距离
                    float beginY = event.getY();

                    if (beginY < mC1Y2) {
                        //起始点在选择框上部,不做反应
                        return false;
                    } else if (beginY > mC1Y2 + mSelectHeight) {
                        //起始点在选择框下部,不做反应
                        return false;
                    }

                    //记录按下的位置和选择区域的上边距的差
                    mDistanceY = beginY - mC1Y2;
                    break;
                case MotionEvent.ACTION_MOVE:

                    //mC1Y1和mC2Y2始终不变
                    //更改子控件坐标
                    mC1Y2 = (int) (event.getY() - mDistanceY);

                    mC2Y1 = (int) (event.getY() - mDistanceY + mSelectHeight);

                    //往上滑动
                    if (mC1Y2 < 0) {
                        //防止顶部超过出

                        //子控件1为0
                        mC1Y2 = 0;

                        //子控件2为最大
                        mC2Y1 = (int) (mSelectHeight);

                    } else if (mC1Y2 > mParentHeight - mSelectHeight) {
                        //防止底部超过

                        //子控件1为最大
                        mC1Y2 = (int) (mParentHeight - mSelectHeight);
                        //子控件2为0
                        mC2Y1 = (int) (mParentHeight);
                    }
                    //重新绘制
                    invalidate();
                    break;
                case MotionEvent.ACTION_UP:
                    //手指抬起
                    break;
                case MotionEvent.ACTION_CANCEL:
                    //事件取消
                    break;
                default:
                    break;
            }
        }

        return true;
    }

7.后续处理。后续处理的话,和项目不一样就不一样的,我们项目是把可选区域的坐标绝对值给后台,后台截取。demo里面是利用Android的截图,然后传去可选区域的坐标截取出来,但是这样分辨率肯定比较低,不太适合做封面。然后就是截取的话,需要注意下有个状态栏高度。(这里有个小坑的地方,就是Android截图系统只有能一张,需要重新加载才能获取新的截图,因为项目没有用到这个,所以没有深入研究,如果有知道的,麻烦赐教,感谢)

    public Bitmap getBitmap(Activity activity) {
        View screenView = activity.getWindow().getDecorView();
        screenView.setDrawingCacheEnabled(true);
        screenView.buildDrawingCache();

        //获取屏幕整张图
        Bitmap bitmap = screenView.getDrawingCache();
        //截图指定部分
        if (bitmap != null) {
            bitmap = Bitmap.createBitmap(bitmap, mC1X1, mC1Y2 + getStatusBarHeight(),
                    (int) mParentWidth, (int) mSelectHeight);
        }
        invalidate();
        return bitmap;
    }

    /**
     * 获取状态栏高度
     */
    private int getStatusBarHeight() {
        int result = 0;
        int resourceId = mContext.getResources().getIdentifier("status_bar_height", "dimen", "android");
        if (resourceId > 0) {
            result = mContext.getResources().getDimensionPixelSize(resourceId);
        }
        return result;
    }

结语

  1. demo已经放到了github:https://github.com/sorgs/DragView上面了,如果能帮到您的话,还麻烦动动小指头给个小星星,万分感谢了!
  2. 本人才疏学浅,仅仅是一个还差一个多月才毕业的应届生,写的比较简单,请大家见谅。如果有什么纰漏和不对的地方,感谢指出。
  3. 对自定义View安利一个学习的地方:http://www.gcssloop.com/customview/CustomViewIndex/,GcsSloop大佬的系列,很受教!
  4. 最后就是我最想说的。其实很多东西看起来很复杂,但是慢慢静下心去做还是可以做出来的。虽然这个很简单,但是我们老大说让我研究下的时候,我也是一脸懵逼啊。心里想,这,我怎么能做得出来。反正研究嘛,做不出来还有老大撑腰。就是就一步一步来尝试,先让动起来,然后再慢慢靠近需求,最后优化。最终发现还是弄出来了。写这篇博客的主要目的就是给自己和大家说这个道理,不畏惧,一步步来!
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值