Android自定義控制元件之區域性圖片放大鏡--BiggerView

張風捷特烈發表於2018-11-19

零、前言:

本文的知識點一覽

1.自定義控制元件及自定義屬性的寫法,你也將對onMesure有更深的認識
2.關於bitmap的簡單處理,及canvas區域裁剪
3.本文會實現兩個自定義控制元件:FitImageView(圖片自適應)BiggerView(放大鏡),前者為後者作為鋪墊。
4.最後會介紹如何從guihub生成自己的依賴庫,這樣一個完整的自定義控制元件庫便ok了。
5.本專案原始碼見文尾捷文規範第一條

實現效果一覽:

1.放大鏡效果1:

9414344-748958617232b4f3.gif
放大鏡效果1.gif

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

9414344-ff733f2bd5499cbb.gif
放大鏡效果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的尺寸即圖片的尺寸,
將藍色作為背景,結果如下,你應該明白是什麼意思了吧,就是既想要圖片不變形,又想不要超出的背景區域:

9414344-5308d12f6f1bf129.png
寬大於高.png
9414344-ad744b38415eb67e.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);
        }
    }
}

二、初級階段

點選的時候生成一個圓球,並隨著手指移動跟隨移動,鬆開手時消失,如圖:
這個小球就是將來展示區域性放大效果的地方

9414344-a7ab3d9439b9ea86.gif
初階效果.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);
    }
}

三、中級階段:(放大圖片的處理)

9414344-748958617232b4f3.gif
放大鏡效果1.gif
9414344-4b3f87e536599a38.png
放大圖平移到觸點.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及以上
一開始觸點是在圓的中心,這裡往上調了一下(理由很簡單,手指太大,把要看的部位遮住了...)
但這有個問題,就是最上面的部分再往上就無法顯示了,使用做了如下的優化:

9414344-0e7e2dba8422056e.gif
優化.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.落點在圖片邊界區域處理:
9414344-b5d1c7cb2729689a.png
矩形區域校驗.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.變成庫!!,變成庫!!,變成庫!!
9414344-58cb802128a63935.png
變成庫.png

1.上傳github
9414344-0b1c6f12ee035dcb.png
上傳github.png

2.釋出:
9414344-e54abdaaacdfcddd.png
1.png
9414344-d058c5cfcd40a767.png
2.png

3.檢視:https://jitpack.io/
9414344-e8b282419b05d06d.png
see1.png
4.測試使用:
9414344-aaff6c5794ddbce4.png
使用.png

ok,本篇完結


後記:捷文規範

1.本文成長記錄及勘誤表
專案原始碼 日期 備註
V0.1--github 2018-11-17 Android自定義控制元件之區域性圖片放大鏡--BiggerView
2.更多關於我
筆名 QQ 微信 愛好
張風捷特烈 1981462002 zdl1994328 語言
我的github 我的簡書 我的掘金 個人網站
3.宣告

1----本文由張風捷特烈原創,轉載請註明
2----歡迎廣大程式設計愛好者共同交流
3----個人能力有限,如有不正之處歡迎大家批評指證,必定虛心改正
4----看到這裡,我在此感謝你的喜歡與支援


9414344-8a0c95a090041a0d.png
icon_wx_200.png

相關文章