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/> * 郵箱:[email protected]<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.檢視: ofollow,noindex">https://jitpack.io/

see1.png
4.測試使用:

使用.png
ok,本篇完結
後記:捷文規範
1.本文成長記錄及勘誤表
專案原始碼 | 日期 | 備註 |
---|---|---|
V0.1--github | 2018-11-17 | Android自定義控制元件之區域性圖片放大鏡--BiggerView |
2.更多關於我
筆名 | 微信 | 愛好 | |
---|---|---|---|
張風捷特烈 | 1981462002 | zdl1994328 | 語言 |
我的github | 我的簡書 | 我的掘金 | 個人網站 |
3.宣告
1----本文由張風捷特烈原創,轉載請註明
2----歡迎廣大程式設計愛好者共同交流
3----個人能力有限,如有不正之處歡迎大家批評指證,必定虛心改正
4----看到這裡,我在此感謝你的喜歡與支援

icon_wx_200.png