1. 程式人生 > >利用 2D 圖形和 PorterDuffXferMode 等實現被遮罩的圖片

利用 2D 圖形和 PorterDuffXferMode 等實現被遮罩的圖片

圖片的遮罩就是將裁剪遮罩應用於圖片或形狀,定義應用中另一張圖片的可見邊界。

利用 2D 圖形和 PorterDuffXferMode,可以將各種遮罩應用於某張點陣圖。

第一張效果圖:

其基本步驟:

1. 建立一個可變的空白 Bitmap 例項,以及在其中繪圖的 Canvas。

2. 首先在 Canvas 上畫好遮罩模式。

3. 將 PorterDuffXferMode 應用到 Paint 上。

4. 用傳輸模式將原圖繪製到 Canvas 上。

其中的關鍵是 PorterDuffXferMode,它會考慮到 Canvas 中已有的資料的狀態和應用到當前操作的圖形資料的狀態。

第一中方法實現遮罩,使用圖片作為 BitmapShader 將內容繪製到另一個元素中。通過這種方式,就可以將圖片畫素視為用於繪製形狀或者元素的“顏色”,這些形狀或者元素將組成遮罩圖片。

RoundedCornerImageView.java :

<span style="font-size:18px;">package com.scxh.imagecover;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapShader;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.RectF;
import android.graphics.Shader;
import android.util.AttributeSet;
import android.view.View;

public class RoundedCornerImageView extends View{

    private Bitmap mImage;
    private Paint mBitmapPaint;
    private RectF mBounds;
    private float mRadius = 25.0f;

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

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

    public RoundedCornerImageView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        init();
    }

    private void init() {
        // 建立圖片塗繪
        mBitmapPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        // 建立作為繪圖邊界的矩形
        mBounds = new RectF();
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int height = 0;
        int width = 0;

        // 所請求大小是圖片內容的大小
        int imageHeight, imageWidth;
        if (mImage == null) {
            imageHeight = imageWidth = 0;
        } else {
            imageHeight = mImage.getHeight();
            imageWidth = mImage.getWidth();
        }

        // 獲得最佳測量值並在檢視上設定該值
        width = getMeasurement(widthMeasureSpec, imageWidth);
        height = getMeasurement(heightMeasureSpec, imageHeight);

        setMeasuredDimension(width, height);
    }

    private int getMeasurement(int measureSpec, int contentSize) {
        int specSize = MeasureSpec.getSize(measureSpec);
        switch (MeasureSpec.getMode(measureSpec)) {
            case MeasureSpec.AT_MOST:
                return Math.min(specSize, contentSize);
            case MeasureSpec.UNSPECIFIED:
                return contentSize;
            case MeasureSpec.EXACTLY:
                return specSize;
            default:
                return 0;
        }
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        if (w != oldw || h != oldh) {
            // 我們要使圖片居中,因此在檢視改變大小時偏移值
            int imageWidth, imageHeight;
            if (mImage == null) {
                imageWidth = imageHeight = 0;
            } else {
                imageWidth = mImage.getWidth();
                imageHeight = mImage.getHeight();
            }
            int left = (w - imageWidth) / 2;
            int top = (h - imageHeight) / 2;

            // 設定邊界以偏移圓角矩形(整個圖形居中)
            mBounds.set(left, top, left+imageWidth, top+imageHeight);

            // 偏移著色器以在矩形內部繪製點陣圖
            // 如果沒有此步驟,點陣圖將在檢視中的(0, 0)處
            if (mBitmapPaint.getShader() != null) {
                Matrix m = new Matrix();
                m.setTranslate(left, top);
                mBitmapPaint.getShader().setLocalMatrix(m);
            }
        }
    }

    /**
     *  供使用者呼叫,並建立一個 BitmapShader 來封裝圖片畫素,並在用於繪圖
     *  的畫筆上進行相應的設定。
     * @param bitmap 點陣圖
     */
    public void setImage(Bitmap bitmap) {
        if (mImage != bitmap) {
            mImage = bitmap;
            if (mImage != null) {
                BitmapShader shader = new BitmapShader(mImage,
                        Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
                mBitmapPaint.setShader(shader);
            } else {
                mBitmapPaint.setShader(null);
            }
            // 繪製成 bitmap
            requestLayout();
        }
    }

    // 讓檢視繪製背景等物件
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        // 使用計算得出的值繪製圖片
        if (mBitmapPaint != null) {
            canvas.drawRoundRect(mBounds, mRadius, mRadius, mBitmapPaint);
        }
    }

}
</span>
該類中關於自定義 view 時,用到的測量等,可以參見我以前的部落格《簡單的完全自定義檢視(同心圓)》:http://blog.csdn.net/antimage08/article/details/50103433點選開啟連結 MainActivity.java  :
<span style="font-size:18px;">package com.scxh.imagecover;

import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;


public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        RoundedCornerImageView imageView = new RoundedCornerImageView(this);
        Bitmap source = BitmapFactory.decodeResource(getResources(), R.drawable.image01);

        imageView.setImage(source);

        setContentView(imageView);

    }

}
</span>
第二中方法實現遮罩:此處採用兩張圖片,一張如上圖的效果所示;另一張採用一個黑色的倒三角形(從 300 * 300 畫素上扣取)。效果如下: 首先在 Canvas 中繪製三角形圖片,這就是圖片的遮罩。然後,在同一個 Canvas 上繪製原圖時應用 PorterDuff.Mode.SRC_IN 轉換,得到的就是帶圓角的原圖。 這是因為 SRC_IN 轉換模式就是告訴 Paint 物件,只在 Canvas 上原圖和目標圖(已經畫好的三角形)重疊視為地方繪製畫素點,畫素點則來自原圖。 在執行 Android 5.0 及更高版本的裝置上,Android 框架支援通過動態陰影表明檢視的提高(通過 elevation 和 translationZ 屬性)。 在簡單的示例中,可以在內部進行處理,但如果應用任意遮罩,則還必須使用匹配的 ViewOutlineProvider 指示在何處產生陰影。 ViewOutlineProvider 有一個必須的方法 getOutline(),如果由於大小或配置發生變化而需要更新輪廓,就會呼叫該方法。 MaskActivity.java :
<span style="font-size:18px;">package com.scxh.imagecover;

import android.app.Activity;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Outline;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.os.Build;
import android.os.Bundle;
import android.view.View;
import android.view.ViewOutlineProvider;
import android.widget.ImageView;


public class MaskActivity extends Activity {

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);


        ImageView imageView = new ImageView(this);
        imageView.setScaleType(ImageView.ScaleType.CENTER);

        // 建立並載入圖片(通常是不可修改的)
        Bitmap source = BitmapFactory.decodeResource(getResources(), R.drawable.image01);
        Bitmap mask = BitmapFactory.decodeResource(getResources(), R.drawable.dsjx);

        // 建立一個可修改的位置以及一個在其中繪製的 Canvas
        final Bitmap result = Bitmap.createBitmap(source.getWidth(),
                source.getHeight(), Bitmap.Config.ARGB_8888);
        Canvas canvas = new Canvas(result);
        Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
        paint.setColor(Color.BLACK);

        canvas.drawBitmap(mask, 0, 0, paint);
        // PorterDuff.Mode.SRC_IN 模式:會根據目標邊界對原圖進行裁剪
        paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
        canvas.drawBitmap(source, 0, 0, paint);
        paint.setXfermode(null);

        imageView.setImageBitmap(result);

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            // 提高檢視以建立可見陰影(數值越大陰影擴散的範圍就越大)
            imageView.setElevation(30f);
            // 繪製匹配遮罩的輪廓,從而提供適當的陰影
            imageView.setOutlineProvider(new ViewOutlineProvider() {
                @Override
                public void getOutline(View view, Outline outline) {
                    int x = (view.getWidth() - result.getWidth()) / 2;
                    int y = (view.getHeight() - result.getHeight()) / 2;

                    Path path = new Path();

                    // 路徑的起始位置(倒三角形的左上角)
                    path.moveTo(x, y);
                    // 沿路徑繪製直線 (倒三角形的右上角)
                    path.lineTo(x + result.getWidth(), y);
                    // 沿路徑繪製直線 (倒三角形的下頂點)
                    path.lineTo(x + result.getWidth() / 2, (float) (y + result.getHeight()/1.6));
                    // 沿路徑繪製直線 (倒三角形的左上角)
                    path.lineTo(x, y);
                    // 繪製成封閉圖形後,關閉路徑
                    path.close();

                    outline.setConvexPath(path);
                }
            });
        }

        setContentView(imageView);

    }
}
</span>

如果輪廓足夠簡單,Android 還可以將其作為檢視的剪下遮罩。只需要呼叫 setClipToOutline(true),即可表明檢視應使用其輪廓作為剪下遮罩。 到Android5.0為止,僅支援通過矩形,圓形和圓角矩形輪廓進行剪下。上圖的三角形就不能用作剪下。 圓形輪廓剪下的效果圖: OutlineActivity.java :
package com.scxh.imagecover;

import android.app.Activity;
import android.graphics.Outline;
import android.os.Bundle;
import android.view.View;
import android.view.ViewOutlineProvider;
import android.widget.ImageView;


public class OutlineActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        final ImageView imageView = new ImageView(this);
        imageView.setScaleType(ImageView.ScaleType.CENTER);

        // 提高檢視以建立可見陰影(數值越大陰影擴散的範圍就越大)
        imageView.setElevation(30f);
        imageView.setImageResource(R.drawable.image02);

        // 告訴檢視使用其輪廓作為剪下遮罩
        imageView.setClipToOutline(true);

        // 為剪下和陰影提供圓形檢視輪廓
        imageView.setOutlineProvider(new ViewOutlineProvider() {
            @Override
            public void getOutline(View view, Outline outline) {

                ImageView mImageView = (ImageView)view;
                int radius = mImageView.getDrawable().getIntrinsicHeight()/2;
                int centerX = (view.getRight() - view.getLeft())/2;
                int centerY = (view.getBottom() - view.getTop())/2;

                outline.setOval(centerX - radius,
                        centerY - radius,
                        centerX + radius,
                        centerY + radius);
            }
        });

        setContentView(imageView);
    }
}