1.自定义控件及自定义属性的写法,你也将对onMesure有更深的认识
2.关于bitmap的简单处理,及canvas区域裁剪
3.本文会实现两个自定义控件:FitImageView(图片自适应)
和BiggerView(放大镜)
,前者为后者作为铺垫。
4.最后会介绍如何从guihub生成自己的依赖库,这样一个完整的自定义控件库便ok了。
5.本项目源码见文尾捷文规范
第一条
1.放大镜效果1:
2.放大镜效果2:(使用了clipOutPath需要API26)
allprojects {
repositories {
...
maven { url 'https://jitpack.io' }
}
}
dependencies {
implementation 'com.github.toly1994328:BiggerView:v1.01'
}
一开始想做放大镜效果,没多想就继承ImageView了,后来越做越困难,bitmap的裁剪模式会影响视图中显示图片的大小。
而View自己的的大小不变,会导致图片显示宽高捕捉困难,和图片左上角捕捉困难。
这就会导致绘制放大图片时的定位适配困难,那么多裁剪模式,想想都崩溃。
于是我想到,自己定义图像显示的view算了,需求是宽高按比例适应,并且View的尺寸即图片的尺寸,
将蓝色作为背景,结果如下,你应该明白是什么意思了吧,就是既想要图片不变形,又想不要超出的背景区域:
<!--图片放大镜-->
<declare-styleable name="FitImageView">
<!--图片资源-->
<attr name="z_fit_src" format="reference"/>
</declare-styleable>
/**
* 作者:张风捷特烈<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
}
@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;
}
创建的时机选择在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);
}
<?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>
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);
}
}
}
点击的时候生成一个圆球,并随着手指移动跟随移动,松开手时消失,如图:
这个小球就是将来展示局部放大效果的地方
private int mBvRadius = dp(30);//半径
private Paint mPaint;//主画笔
private float mCurX;//当前触点X
private float mCurY;//当前触点Y
private boolean isDown;//是否触摸
@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;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (isDown) {
canvas.drawCircle(mCurX, mCurY, mBvRadius, mPaint);
}
}
@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);
}
这里通过定位,将图片移至指定位置
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (isDown) {
canvas.drawBitmap(mBiggerBitmap, -mCurX * (rate - 1), -mCurY * (rate - 1), mPaint);
}
}
这样效果1就完成了
使用了clipOutPath的API,不须26及以上
一开始触点是在圆的中心,这里往上调了一下(理由很简单,手指太大,把要看的部位遮住了...)
但这有个问题,就是最上面的部分再往上就无法显示了,使用做了如下的优化:
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);
}
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();
}
//可拓展更多模式....
}
}
}
@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;
}
3.查看:https://jitpack.io/
ok,本篇完结
项目源码 | 日期 | 备注 |
---|---|---|
V0.1--github | 2018-11-17 | Android自定义控件之局部图片放大镜--BiggerView |
笔名 | 微信 | 爱好 | |
---|---|---|---|
张风捷特烈 | 1981462002 | zdl1994328 | 语言 |
我的github | 我的简书 | 我的掘金 | 个人网站 |
1----本文由张风捷特烈原创,转载请注明
2----欢迎广大编程爱好者共同交流
3----个人能力有限,如有不正之处欢迎大家批评指证,必定虚心改正
4----看到这里,我在此感谢你的喜欢与支持