Android自定义控件之局部图片放大镜--BiggerView

零、前言:

本文的知识点一览

1.自定义控件及自定义属性的写法,你也将对onMesure有更深的认识 2.关于bitmap的简单处理,及canvas区域裁剪 3.本文会实现两个自定义控件:FitImageView(图片自适应)BiggerView(放大镜),前者为后者作为铺垫。 4.最后会介绍如何从guihub生成自己的依赖库,这样一个完整的自定义控件库便ok了。 5.本项目源码见文尾捷文规范第一条

实现效果一览:

1.放大镜效果1:

放大镜效果1.gif

2.放大镜效果2:(使用了clipOutPath需要API26)

放大镜效果2.gif

3.该控件已做成类库(欢迎star),使用:
    allprojects {
        repositories {
            ...
            maven { url 'https://jitpack.io' }
        }
    }
    
    dependencies {
            implementation 'com.github.toly1994328:BiggerView:v1.01'
    }

一、宽高等比例自适应的控件:FitImageView

一开始想做放大镜效果,没多想就继承ImageView了,后来越做越困难,bitmap的裁剪模式会影响视图中显示图片的大小。 而View自己的的大小不变,会导致图片显示宽高捕捉困难,和图片左上角捕捉困难。 这就会导致绘制放大图片时的定位适配困难,那么多裁剪模式,想想都崩溃。 于是我想到,自己定义图像显示的view算了,需求是宽高按比例适应,并且View的尺寸即图片的尺寸, 将蓝色作为背景,结果如下,你应该明白是什么意思了吧,就是既想要图片不变形,又想不要超出的背景区域:

宽大于高.png

高大于宽.png


1.自定义属性:
    <!--图片放大镜-->
    <declare-styleable name="FitImageView">
        <!--图片资源-->
        <attr name="z_fit_src" format="reference"/>
    </declare-styleable>
2.自定义控件初始代码
/**
 * 作者:张风捷特烈<br/>
 * 时间:2018/11/19 0019:0:14<br/>
 * 邮箱:1981462002@qq.com<br/>
 * 说明:宽高自适应图片视图
 */
public class FitImageView extends View {

    private Paint mPaint;//主画笔
    private Drawable mFitSrc;//自定义属性获取的Drawable
    private Bitmap mBitmapSrc;//源图片
    protected Bitmap mFitBitmap;//适应宽高的缩放图片

    protected float scaleRateW2fit = 1;//宽度缩放适应比率
    protected float scaleRateH2fit = 1;//高度缩放适应比率
    protected int mImageW, mImageH;//图片显示的宽高

    public FitImageView(Context context) {
        this(context, null);
    }

    public FitImageView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs,0);
    }

    public FitImageView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.FitImageView);
        mFitSrc = a.getDrawable(R.styleable.FitImageView_z_fit_src);
        a.recycle();
        init();//初始化
    }

    private void init() {
        //初始化主画笔
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mBitmapSrc = ((BitmapDrawable) mFitSrc).getBitmap();//获取图片
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //TODO draw
    }
3.测量及摆放:(这是核心处理)
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    mImageW = dealWidth(widthMeasureSpec);//显示图片宽
    mImageH = dealHeight(heightMeasureSpec);//显示图片高
    float bitmapWHRate = mBitmapSrc.getHeight() * 1.f / mBitmapSrc.getWidth();//图片宽高比
    if (mImageH >= mImageW) {
        mImageH = (int) (mImageW * bitmapWHRate);//宽小,以宽为基准
    } else {
       mImageW = (int) (mImageH / bitmapWHRate);//高小,以高为基准
    }
    setMeasuredDimension(mImageW, mImageH);
}


/**
 * @param heightMeasureSpec
 * @return
 */
private int dealHeight(int heightMeasureSpec) {
    int result = 0;
    int mode = MeasureSpec.getMode(heightMeasureSpec);
    int size = MeasureSpec.getSize(heightMeasureSpec);
    if (mode == MeasureSpec.EXACTLY) {
        //控件尺寸已经确定:如:
        // android:layout_height="40dp"或"match_parent"
        scaleRateH2fit = size * 1.f / mBitmapSrc.getHeight() * 1.f;
        result = size;
    } else {
        result = mBitmapSrc.getHeight();
        if (mode == MeasureSpec.AT_MOST) {//最多不超过
            result = Math.min(result, size);

        }
    }
    return result;
}


/**
 * @param widthMeasureSpec
 */
private int dealWidth(int widthMeasureSpec) {
    int result = 0;
    int mode = MeasureSpec.getMode(widthMeasureSpec);
    int size = MeasureSpec.getSize(widthMeasureSpec);
    if (mode == MeasureSpec.EXACTLY) {
        //控件尺寸已经确定:如:
        // android:layout_XXX="40dp"或"match_parent"
        scaleRateW2fit = size * 1.f / mBitmapSrc.getWidth();
        result = size;

    } else {
        result = mBitmapSrc.getWidth();
        if (mode == MeasureSpec.AT_MOST) {//最多不超过
            result = Math.min(result, size);
        }
    }
    return result;
}
4.创建缩放后的bitmap及绘制

创建的时机选择在onLayout里,因为要先测量后才能知道缩放比

@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    super.onLayout(changed, left, top, right, bottom);
    mFitBitmap = createBigBitmap(Math.min(scaleRateW2fit, scaleRateH2fit), mBitmapSrc);
    mBitmapSrc = null;//原图已无用将原图置空
}

/**
 * 创建一个rate倍的图片
 *
 * @param rate 缩放比率
 * @param src  图片源
 * @return 缩放后的图片
 */
protected Bitmap createBigBitmap(float rate, Bitmap src) {
    Matrix matrix = new Matrix();
    //设置变换矩阵:扩大3倍
    matrix.setValues(new float[]{
            rate, 0, 0,
            0, rate, 0,
            0, 0, 1
    });
    return Bitmap.createBitmap(src, 0, 0,
            src.getWidth(), src.getHeight(), matrix, true);
}

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    canvas.drawBitmap(mFitBitmap, 0, 0, mPaint);
}

一、自定义控件:BiggerView

1.自定义属性:attrs.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <!--图片放大镜-->
    <declare-styleable name="BiggerView">
        <!--半径-->
        <attr name="z_bv_radius" format="dimension"/>
        <!--边线宽-->
        <attr name="z_bv_outline_width" format="dimension"/>
        <!--进度色-->
        <attr name="z_bv_outline_color" format="color"/>
        <!--放大倍率-->
        <attr name="z_bv_rate" format="float"/>
    </declare-styleable>
</resources>
2.初始化自定义控件
public class BiggerView extends FitImageView {
    private int mBvRadius = dp(30);//半径
    private int mBvOutlineWidth = 2;//边线宽

    private float rate = 4;//默认放大的倍数
    private int mBvOutlineColor = 0xffCCDCE4;//边线颜色

    private Paint mPaint;//主画笔
    private Bitmap mBiggerBitmap;//放大的图片
    private Path mPath;//剪切路径

    public BiggerView(Context context) {
        this(context, null);
    }

    public BiggerView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public BiggerView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.BiggerView);
        mBvRadius = (int) a.getDimension(R.styleable.BiggerView_z_bv_radius, mBvRadius);
        mBvOutlineWidth = (int) a.getDimension(R.styleable.BiggerView_z_bv_outline_width, mBvOutlineWidth);
        mBvOutlineColor = a.getColor(R.styleable.BiggerView_z_bv_outline_color, mBvOutlineColor);
        rate = (int) a.getFloat(R.styleable.BiggerView_z_bv_rate, rate);
        a.recycle();
        init();
    }

    private void init() {
        //初始化主画笔
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setColor(mBvOutlineColor);
        mPaint.setStrokeWidth(mBvOutlineWidth * 2);
        mPath = new Path();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        }
    }
}

二、初级阶段

点击的时候生成一个圆球,并随着手指移动跟随移动,松开手时消失,如图: 这个小球就是将来展示局部放大效果的地方

初阶效果.gif

1.添加成员变量:
private int mBvRadius = dp(30);//半径
private Paint mPaint;//主画笔

private float mCurX;//当前触点X
private float mCurY;//当前触点Y
private boolean isDown;//是否触摸
2.触点的处理
@Override
public boolean onTouchEvent(MotionEvent event) {
    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
        case MotionEvent.ACTION_MOVE:
            isDown = true;
            mCurX = event.getX();
            mCurY = event.getY();
            break;
        case MotionEvent.ACTION_UP:
            isDown = false;
    }
    invalidate();//记得刷新
    return true;
}
3.绘制
@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    if (isDown) {
        canvas.drawCircle(mCurX, mCurY, mBvRadius, mPaint);
    }
}

三、中级阶段:(放大图片的处理)

放大镜效果1.gif

放大图平移到触点.png

1.在onLayout时创建一个rate倍大小的Bitmap
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    super.onLayout(changed, left, top, right, bottom);
    mBiggerBitmap = createBigBitmap(rate, mFitBitmap);
}
2.绘制比放大后的图

这里通过定位,将图片移至指定位置

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if (isDown) {
            canvas.drawBitmap(mBiggerBitmap, -mCurX * (rate - 1), -mCurY * (rate - 1), mPaint);
        }
    }

这样效果1就完成了


3.效果2的实现:

使用了clipOutPath的API,不须26及以上 一开始触点是在圆的中心,这里往上调了一下(理由很简单,手指太大,把要看的部位遮住了...) 但这有个问题,就是最上面的部分再往上就无法显示了,使用做了如下的优化:

优化.gif

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
    mShowY = -mCurY * (rate - 1) - 2 * mBvRadius;
    canvas.drawBitmap(mBiggerBitmap,
            -mCurX * (rate - 1), mShowY, mPaint);
    float rY = mCurY > 2 * mBvRadius ? mCurY - 2 * mBvRadius : mCurY +  mBvRadius;
    mPath.addCircle(mCurX, rY, mBvRadius, Path.Direction.CCW);
    canvas.clipOutPath(mPath);
    super.onDraw(canvas);
    canvas.drawCircle(mCurX, rY, mBvRadius, mPaint);
}

四、高级阶段:优化点:

1.使用枚举切换放大镜类型:
enum Style {
    NO_CLIP,//无裁剪,直接放大
    CLIP_CIRCLE,//圆形裁剪
}

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    if (isDown) {
        switch (mStyle) {
            case NO_CLIP://无裁剪,直接放大
                float showY = -mCurY * (rate - 1);
                canvas.drawBitmap(mBiggerBitmap, -mCurX * (rate - 1), showY, mPaint);
                break;
            case CLIP_CIRCLE:
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                    mPath.reset();
                    showY = -mCurY * (rate - 1) - 2 * mBvRadius;
                    canvas.drawBitmap(mBiggerBitmap, -mCurX * (rate - 1), showY, mPaint);
                    float rY = mCurY > 2 * mBvRadius ? mCurY - 2 * mBvRadius : mCurY + mBvRadius;
                    mPath.addCircle(mCurX, rY, mBvRadius, Path.Direction.CCW);
                    canvas.clipOutPath(mPath);
                    super.onDraw(canvas);
                    canvas.drawCircle(mCurX, rY, mBvRadius, mPaint);
                } else {
                    mStyle = Style.NO_CLIP;//如果版本过低,无裁剪,直接放大
                    invalidate();
                }
                //可拓展更多模式....
        }
    }
}
2.落点在图片边界区域处理:

矩形区域校验.png

@Override
public boolean onTouchEvent(MotionEvent event) {
    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
        case MotionEvent.ACTION_MOVE:
            mCurX = event.getX();
            mCurY = event.getY();
            //校验矩形区域
            isDown = judgeRectArea(mImageW / 2, mImageH / 2, mCurX, mCurY, mImageW, mImageH);
            break;
        case MotionEvent.ACTION_UP:
            isDown = false;
    }
    invalidate();//记得刷新
    return true;
}

/**
 * 判断落点是否在矩形区域
 */
public static boolean judgeRectArea(float srcX, float srcY, float dstX, float dstY, float w, float h) {
    return Math.abs(dstX - srcX) < w / 2 && Math.abs(dstY - srcY) < h / 2;
}

五、上传github并成库

0.变成库!!,变成库!!,变成库!!

变成库.png


1.上传github

上传github.png


2.发布:

1.png

2.png


3.查看:https://jitpack.io/

see1.png

4.测试使用:

使用.png

ok,本篇完结

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏项勇

笔记51 | Android自定义View(二)

20060
来自专栏肖蕾的博客

第六章:常用控件日常科普标签(Lable)图片(Image)按钮(Button)

1.控件是用于开发构建用户界面(UI)控件,帮助完成开发中视窗,文本框,按钮,下拉菜单,等界面元素 2.在LibGdx中,提供的控件有 按钮,勾选框,下拉框,...

12320
来自专栏向治洪

AnimatedPathView实现自定义图片标签

老早用过小红书app,对于他们客户端笔记这块的设计非常喜欢,恰好去年在小红书的竞争对手公司,公司基于产品的考虑和产品的发展,也需要将app社交化,于是在社区分享...

219100
来自专栏向治洪

自定义圆角和园边的实现

本来想在网上找个圆角的例子看一看,不尽人意啊,基本都是官方的Demo的那张原理图,稍后会贴出。于是自己自定义了个View,实现图片的圆角以及圆形效果。效果图: ...

22870
来自专栏Android源码框架分析

Android自定义View:MeasureSpec的真正意义与View大小控制

自定义View是Android开发中最普通的需求,灵活控制View的尺寸是开发者面临的第一个问题,比如,为什么明明使用的是WRAP_CONTENT却跟MATCH...

15020
来自专栏Android干货园

Android带你解析ScrollView--仿QQ空间标题栏渐变

版权声明:本文为博主原创文章,转载请标明出处。 https://blog.csdn.net/lyhhj/article/details/52...

26710
来自专栏Android开发指南

15.瀑布流、测量

30570
来自专栏肖蕾的博客

Android自定义控件之圆形进度条Android自定义控件之-圆形进度条

1.9K50
来自专栏封碎

Android画图之抗锯齿 博客分类: Android小技巧 Android

    在画图的时候,图片如果旋转或缩放之后,总是会出现那些华丽的锯齿。其实Android自带了解决方式。     方法一:给Paint加上抗锯齿标志。然后将...

28920
来自专栏Hellovass 的博客

手动测量 View 的宽高

手动调用 View 的 measure(int widthMeasureSpec,int heightMeasureSpec) 方法来得到 View 的宽高。

23360

扫码关注云+社区

领取腾讯云代金券