利用 2D 图形和 PorterDuffXferMode 等实现被遮罩的图片

时间:2023-02-09 15:07:04

图片的遮罩就是将裁剪遮罩应用于图片或形状,定义应用中另一张图片的可见边界。

利用 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 像素上扣取)。效果如下: 利用 2D 图形和 PorterDuffXferMode 等实现被遮罩的图片

首先在 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为止,仅支持通过矩形,圆形和圆角矩形轮廓进行剪切。上图的三角形就不能用作剪切。 圆形轮廓剪切的效果图: 利用 2D 图形和 PorterDuffXferMode 等实现被遮罩的图片


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);
}
}