经常会在移动应用中看到类似下图的各种图片:
这样的图形在Android上要怎么实现呢?在Android系统中,目前主要有三种方式可以实现上图的形状,下面一一介绍。
一、PorterDuffXfermode方式
之前的博客曾经介绍过用这种方式画圆形头像,实际上,它不仅可以用来画圆形头像,还可以实现任意形状。首先来复习一下16中效果:
1.PorterDuff.Mode.CLEAR 所绘制不会提交到画布上
2.PorterDuff.Mode.SRC 显示上层绘制图片
3.PorterDuff.Mode.DST 显示下层绘制图片
4.PorterDuff.Mode.SRC_OVER 正常绘制显示,上下层绘制叠盖。
5.PorterDuff.Mode.DST_OVER 上下层都显示。下层居上显示。
6.PorterDuff.Mode.SRC_IN 取两层绘制交集。显示上层。
7.PorterDuff.Mode.DST_IN 取两层绘制交集。显示下层。
8.PorterDuff.Mode.SRC_OUT 取上层绘制非交集部分。
9.PorterDuff.Mode.DST_OUT 取下层绘制非交集部分。
10.PorterDuff.Mode.SRC_ATOP 取下层非交集部分与上层交集部分
11.PorterDuff.Mode.DST_ATOP 取上层非交集部分与下层交集部分
12.PorterDuff.Mode.XOR 异或:去除两图层交集部分
13.PorterDuff.Mode.DARKEN 取两图层全部区域,交集部分颜色加深
14.PorterDuff.Mode.LIGHTEN 取两图层全部,点亮交集部分颜色
15.PorterDuff.Mode.MULTIPLY 取两图层交集部分叠加后颜色
16.PorterDuff.Mode.SCREEN 取两图层全部区域,交集部分变为透明色
我们的实现思路就是绘制两个图层,利用蒙版效果,取两个图形的交集,就可以实现上图的各种形状。所以,这里我们要用到属性SRC_IN或DST_IN,并需要两个图层,一个是形状图层,一个是显示图层,并且显示图层完全覆盖形状图层。那么,如果形状图层在,显示图层在上,就应该选择SRC_IN属性。
采用PorterDuffXfermode方式的源码如下:
public class IrregularShapeImageView extends android.support.v7.widget.AppCompatImageView {
private Bitmap backgroundBitmap;
private Bitmap mBitmap;
private int viewWidth;
private int viewHeight;
public IrregularShapeImageView(Context context) {
this(context, null, 0);
}
public IrregularShapeImageView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public IrregularShapeImageView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
viewWidth = MeasureSpec.getSize(widthMeasureSpec);
viewHeight = MeasureSpec.getSize(heightMeasureSpec);
}
@Override
protected void onDraw(Canvas canvas) {
if (mBitmap != null && backgroundBitmap != null) {
int min = Math.min(viewWidth, viewHeight);
backgroundBitmap = Bitmap.createScaledBitmap(backgroundBitmap, min, min, false);
mBitmap = Bitmap.createScaledBitmap(mBitmap, min, min, false);
canvas.drawBitmap(createImage(), 0, 0, null);
}
}
private Bitmap createImage() {
Paint paint = new Paint();
paint.setAntiAlias(true);
Bitmap finalBmp = Bitmap.createBitmap(viewWidth, viewHeight, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(finalBmp);
canvas.drawBitmap(backgroundBitmap, 0, 0, paint);
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
canvas.drawBitmap(mBitmap, 0, 0, paint);
return finalBmp;
}
@Override
public void setImageBitmap(Bitmap bm) {
super.setImageBitmap(bm);
mBitmap = bm;
setBitmaps();
}
@Override
public void setImageDrawable(Drawable drawable) {
super.setImageDrawable(drawable);
mBitmap = getBitmapFromDrawable(drawable);
setBitmaps();
}
@Override
public void setImageResource(int resId) {
super.setImageResource(resId);
mBitmap = getBitmapFromDrawable(getDrawable());
setBitmaps();
}
@Override
public void setImageURI(Uri uri) {
super.setImageURI(uri);
mBitmap = getBitmapFromDrawable(getDrawable());
setBitmaps();
}
private void setBitmaps() {
if (null == getBackground()) {
throw new IllegalArgumentException(String.format("background is null."));
} else {
backgroundBitmap = getBitmapFromDrawable(getBackground());
invalidate();
}
}
private Bitmap getBitmapFromDrawable(Drawable drawable) {
super.setScaleType(ScaleType.CENTER_CROP);
if (drawable == null) {
return null;
}
if (drawable instanceof BitmapDrawable) {
return ((BitmapDrawable) drawable).getBitmap();
}
try {
Bitmap bitmap;
bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
drawable.draw(canvas);
return bitmap;
} catch (OutOfMemoryError e) {
return null;
}
}
二、BitmapShader方式
Google官方的说法:Shader used to draw a bitmap as a texture. The bitmap can be repeated or mirrored by setting the tiling mode.
意思就是把bitmap当做纹理来用,可以重复,镜像,其实还有一种,拉伸。
BitmapShader 的构造函数
public BitmapShader(@NonNull Bitmap bitmap, TileMode tileX, TileMode tileY)
三个参数:bitmap 指的是要作为纹理的图片,tileX 指的是在x方向纹理的绘制模式,tileY 指的是Y方向上的绘制模式。
使用BitmapShader实现不规则图形的源码如下:
public class MultiView extends android.support.v7.widget.AppCompatImageView {
/**
* 图片的类型,圆形or圆角or多边形
*/
private Context mContext;
/**
* 传输类型
*/
private int type;
/**
* 圆形
*/
public static final int TYPE_CIRCLE = 0;
/**
* 圆角
*/
public static final int TYPE_ROUND = 1;
/**
* 多边形
*/
public static final int TYPE_MULTI = 3;
/*
* 外多边形
*/
public static final int TYPE_MULTI2 = 4;
/**
* 默认多边形角的个数
*/
public static final int ANGLECOUNT = 5;
/**
* 默认开始绘制的角度
*/
public static final int CURRENTANGLE = 180;
/**
* 多边形的半径
*/
private int startRadius;
/**
* 多边形角的个数
*/
private int angleCount;
private int[] angles;
/**
* 开始绘制的角度
*/
private int currentAngle;
/**
* 存储角位置的集合
*/
private List<PointF> pointFList = new ArrayList<>();
/**
* 圆角大小的默认值
*/
private static final int BODER_RADIUS_DEFAULT = 10;
/**
* 圆角的大小
*/
private int mBorderRadius;
/**
* 绘图的Paint
*/
private Paint mBitmapPaint;
/**
* 圆角的半径
*/
private int mRadius;
/**
* 3x3 矩阵,主要用于缩小放大
*/
private Matrix mMatrix;
/**
* 渲染图像,使用图像为绘制图形着色
*/
private BitmapShader mBitmapShader;
/**
* view的宽度
*/
private int mWidth;
private RectF mRoundRect;
public MultiView(Context context) {
this(context, null);
}
public MultiView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public MultiView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
this.mContext = context;
init(context, attrs);
}
public void init(Context context, AttributeSet attrs) {
mMatrix = new Matrix();
mBitmapPaint = new Paint();
mBitmapPaint.setAntiAlias(true);
TypedArray typedArray = context.obtainStyledAttributes(attrs,
R.styleable.RoundImageView);
mBorderRadius = typedArray.getDimensionPixelSize(
R.styleable.RoundImageView_borderRadius, (int) TypedValue
.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
BODER_RADIUS_DEFAULT, getResources()
.getDisplayMetrics()));// 默认为10dp
type = typedArray.getInt(R.styleable.RoundImageView_type, TYPE_CIRCLE);// 默认为Circle
angleCount = typedArray.getInt(R.styleable.RoundImageView_angleCount, ANGLECOUNT);
currentAngle = typedArray.getInt(R.styleable.RoundImageView_currentAngle, currentAngle);
typedArray.recycle(); //回收之后对象可以重用
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
/**
* 如果类型是圆形或多边形,则强制改变view的宽高一致,以小值为准
*/
if (type == TYPE_CIRCLE) {
mWidth = Math.min(getMeasuredWidth(), getMeasuredHeight());
mRadius = mWidth / 2;
setMeasuredDimension(mWidth, mWidth);
}
if (type == TYPE_MULTI || type == TYPE_MULTI2) {
mWidth = Math.min(getMeasuredWidth(), getMeasuredHeight());
setMeasuredDimension(mWidth, mWidth);
angles = new int[angleCount];
for (int i = 0; i < angleCount; i++) {
int partOfAngle = 360 / angleCount; //每个顶点的角度
angles[i] = currentAngle + partOfAngle * i;
startRadius = mWidth / 2;
float x = (float) (Math.sin(Math.toRadians(angles[i])) * startRadius);
float y = (float) (Math.cos(Math.toRadians(angles[i])) * startRadius);
pointFList.add(new PointF(x, y));
}
}
}
/**
* 初始化BitmapShader
*/
private void setUpShader() {
Drawable drawable = getDrawable();
if (drawable == null) {
return;
}
Bitmap bmp = drawableToBitamp(drawable);
// 将bmp作为着色器,就是在指定区域内绘制bmp
mBitmapShader = new BitmapShader(bmp, Shader.TileMode.REPEAT, Shader.TileMode.REPEAT);
float scale = 1.0f;
if (type == TYPE_CIRCLE) {
// 拿到bitmap宽或高的小值
int bSize = Math.min(bmp.getWidth(), bmp.getHeight());
scale = mWidth * 1.0f / bSize;
} else if (type == TYPE_ROUND) {
if (!(bmp.getWidth() == getWidth() && bmp.getHeight() == getHeight())) {
// 如果图片的宽或者高与view的宽高不匹配,计算出需要缩放的比例;缩放后的图片的宽高,一定要大于我们view的宽高;所以我们这里取大值;
scale = Math.max(getWidth() * 1.0f / bmp.getWidth(), getHeight() * 1.0f / bmp.getHeight());
}
} else if (type == TYPE_MULTI || type == TYPE_MULTI2) {
// 拿到bitmap宽或高的小值
int bSize = Math.min(bmp.getWidth(), bmp.getHeight());
scale = mWidth * 1.0f / bSize;
}
// shader的变换矩阵,我们这里主要用于放大或者缩小
mMatrix.setScale(scale, scale);
// 设置变换矩阵
mBitmapShader.setLocalMatrix(mMatrix);
// 设置shader
mBitmapPaint.setShader(mBitmapShader);
}
@Override
protected void onDraw(Canvas canvas) {
if (getDrawable() == null) {
return;
}
setUpShader();
if (type == TYPE_ROUND) {
canvas.drawRoundRect(mRoundRect, mBorderRadius, mBorderRadius,
mBitmapPaint);
} else if (type == TYPE_MULTI) {
//canvas.translate(startRadius,startRadius);
Path mPath = drawPath();
canvas.drawPath(mPath, mBitmapPaint);
} else if (type == TYPE_MULTI2) {
Path mPath = drawPath2();
canvas.drawPath(mPath, mBitmapPaint);
} else {
canvas.drawCircle(mRadius, mRadius, mRadius, mBitmapPaint);
}
}
/**
* @return 多边形路径
*/
private Path drawPath2() {
Path mPath = new Path();
mPath.moveTo(pointFList.get(0).x, pointFList.get(0).y);
for (int i = 1; i < angleCount; i++) {
mPath.lineTo(pointFList.get(i).x, pointFList.get(i).y);
}
mPath.offset(startRadius, startRadius);
return mPath;
}
/**
* @return 多边形路径
*/
private Path drawPath() {
Path mPath = new Path();
mPath.moveTo(pointFList.get(0).x, pointFList.get(0).y);
for (int i = 2; i < angleCount; i++) {
if (i % 2 == 0) {// 除以二取余数,余数为0则为偶数,否则奇数
mPath.lineTo(pointFList.get(i).x, pointFList.get(i).y);
}
}
if (angleCount % 2 == 0) { //偶数,moveTo
mPath.moveTo(pointFList.get(1).x, pointFList.get(1).y);
} else { //奇数,lineTo
mPath.lineTo(pointFList.get(1).x, pointFList.get(1).y);
}
for (int i = 3; i < angleCount; i++) {
if (i % 2 != 0) {
mPath.lineTo(pointFList.get(i).x, pointFList.get(i).y);
}
}
mPath.offset(startRadius, startRadius);
return mPath;
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
// 圆角图片的范围
if (type == TYPE_ROUND)
mRoundRect = new RectF(0, 0, w, h);
}
/**
* drawable转bitmap
*
* @param drawable
* @return
*/
private Bitmap drawableToBitamp(Drawable drawable) {
if (drawable instanceof BitmapDrawable) {
BitmapDrawable bd = (BitmapDrawable) drawable;
return bd.getBitmap();
}
int w = drawable.getIntrinsicWidth();
int h = drawable.getIntrinsicHeight();
Bitmap bitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
drawable.setBounds(0, 0, w, h);
drawable.draw(canvas);
return bitmap;
}
private static final String STATE_INSTANCE = "state_instance";
private static final String STATE_TYPE = "state_type";
private static final String STATE_BORDER_RADIUS = "state_border_radius";
@Override
protected Parcelable onSaveInstanceState() {
Bundle bundle = new Bundle();
bundle.putParcelable(STATE_INSTANCE, super.onSaveInstanceState());
bundle.putInt(STATE_TYPE, type);
bundle.putInt(STATE_BORDER_RADIUS, mBorderRadius);
return bundle;
}
@Override
protected void onRestoreInstanceState(Parcelable state) {
if (state instanceof Bundle) {
Bundle bundle = (Bundle) state;
super.onRestoreInstanceState(((Bundle) state)
.getParcelable(STATE_INSTANCE));
this.type = bundle.getInt(STATE_TYPE);
this.mBorderRadius = bundle.getInt(STATE_BORDER_RADIUS);
} else {
super.onRestoreInstanceState(state);
}
}
public void setType(int type) {
if (this.type != type) {
this.type = type;
if (this.type != TYPE_ROUND && this.type != TYPE_CIRCLE && this.type != TYPE_MULTI && this.type != TYPE_MULTI2) {
this.type = TYPE_CIRCLE;
}
requestLayout();
}
}
}
自定义属性如下:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="RoundImageView">
<attr name="borderRadius" format="dimension" />
<attr name="type">
<enum name="circle" value="0" />
<enum name="round" value="1" />
<enum name="multi" value="3" />
<enum name="multi2" value="4" />
</attr>
<attr name="angleCount" format="integer" />
<attr name="currentAngle" format="integer" />
</declare-styleable>
</resources>
三、ClipPath方式
Canvas类的ClipPath方法:
boolean clipPath(Path path)
Intersect the current clip with the specified path.
clipPath方法的作用是“切割画布”。利用这个特性,也可以实现不规则形状。比如,实现一个三角形的代码如下:
public class TriangleView extends android.support.v7.widget.AppCompatImageView {
private Context mContext;
private int mWidth;
private int mHeight;
// 三角形的模式以直角点所在的点位置为模式名
public static final int TYPE_LEFT_TOP = 0;
public static final int TYPE_RIGHT_BOTTOM = 1;
public static final int TYPE_RIGHT_TOP = 2;
public static final int TYPE_RIGHT_MIDDLE = 3;
public static final int TYPE_LEFT_BOTTOM = 4;
public static final int TYPE_RIGHT_BOTTOM_SMAILL = 5;
private int mMode;
private int mColor;
private String mText;
// 三个点的顺序,从leftTop开始计算,顺时针数过去,依次三个点
private int mPointOne[];
private int mPointTwo[];
private int mPointThree[];
private Path mPath;
private Path mTextPath;
private Paint mPaint;
private boolean isClicked = false; //是否被按下
public TriangleView(Context context, AttributeSet attrs) {
super(context, attrs);
mContext = context;
mPath = new Path();
mTextPath = new Path();
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mColor = Color.parseColor("#FFFFFF");
mPaint.setColor(mColor);
}
public void setMode(int model) {
mMode = model;
postInvalidate();
}
public void setColor(int color) {
mColor = color;
postInvalidate();
}
public void setText(String text) {
mText = text;
}
@Override
protected void onDraw(Canvas canvas) {
canvas.clipPath(mPath,INTERSECT);
super.onDraw(canvas);
if(!isClicked) {
mPaint.setColor(Color.argb(180, 0, 0, 0));
canvas.drawPath(mPath, mPaint);
}
if(mText != null) {
mPaint.setFakeBoldText(true);
mPaint.setTextSize(sp2px(mContext,13));
mPaint.setColor(mColor);
float tw = mPaint.measureText(mText);
float th = mPaint.measureText(mText)/mText.length();
switch (mMode) {
case TYPE_LEFT_TOP:
canvas.drawText(mText, (mWidth - tw) / 5f, mHeight / 3f, mPaint);
break;
case TYPE_RIGHT_BOTTOM:
canvas.drawText(mText, (mWidth - tw)*4/5f, mHeight*4/5f, mPaint);
break;
case TYPE_RIGHT_TOP:
canvas.drawText(mText, (mWidth - tw)*3/4f, mHeight/3f, mPaint);
break;
case TYPE_RIGHT_MIDDLE:
canvas.drawText(mText, (mWidth*0.92f - tw)/2f, (mHeight+th)/2f, mPaint);
break;
case TYPE_LEFT_BOTTOM:
mTextPath.moveTo((1.4f*mHeight-tw-2*th)/2.8f,(1.4f*mHeight-tw+2*th)/2.8f);
mTextPath.lineTo((1.4f*mHeight+tw-2*th)/2.8f,(1.4f*mHeight+tw+2*th)/2.8f);
canvas.drawTextOnPath(mText,mTextPath,0, 0, mPaint);
break;
case TYPE_RIGHT_BOTTOM_SMAILL:
mTextPath.moveTo((1.4f*mHeight-tw+2*th)/2.8f,(1.4f*mHeight+tw+2*th)/2.8f);
mTextPath.lineTo((1.4f*mHeight+tw+2*th)/2.8f,(1.4f*mHeight-tw+2*th)/2.8f);
canvas.drawTextOnPath(mText,mTextPath,0, 0, mPaint);
break;
}
}
}
@Override
protected void onSizeChanged(int width, int height, int oldw, int oldh) {
super.onSizeChanged(width, height, oldw, oldh);
mWidth = width;
mHeight = height;
switch (mMode) {
case TYPE_LEFT_TOP:
mPointOne = new int[] { 0, 0 };
mPointTwo = new int[] { width, 0 };
mPointThree = new int[] { 0, height };
break;
case TYPE_RIGHT_BOTTOM:
mPointOne = new int[] { width, 0 };
mPointTwo = new int[] { width, height };
mPointThree = new int[] { 0, height };
break;
case TYPE_RIGHT_TOP:
mPointOne = new int[] { 0, 0 };
mPointTwo = new int[] { width, 0 };
mPointThree = new int[] { width, height };
break;
case TYPE_RIGHT_MIDDLE:
mPointOne = new int[] { 0, 0 };
mPointTwo = new int[] { width, height / 2 };
mPointThree = new int[] { 0, height };
break;
case TYPE_LEFT_BOTTOM:
mPointOne = new int[] { 0, 0 };
mPointTwo = new int[] { width, height};
mPointThree = new int[] { 0, height };
break;
case TYPE_RIGHT_BOTTOM_SMAILL:
mPointOne = new int[] { width, 0 };
mPointTwo = new int[] { width, height };
mPointThree = new int[] { 0, height };
break;
}
if (null != mPointOne) {
mPath.moveTo(mPointOne[0], mPointOne[1]);
mPath.lineTo(mPointTwo[0], mPointTwo[1]);
mPath.lineTo(mPointThree[0], mPointThree[1]);
mPath.close();
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
RectF bounds = new RectF();
mPath.computeBounds(bounds, true);
Region region = new Region();
region.setPath(mPath, new Region((int)bounds.left, (int)bounds.top,(int)bounds.right, (int)bounds.bottom));
boolean ct = region.contains((int)event.getX(), (int)event.getY());
if(event.getAction() == MotionEvent.ACTION_DOWN){
if(ct){
isClicked = true;
invalidate();
return true;
}
return false;
}else if(event.getAction() == MotionEvent.ACTION_UP || event.getAction() == MotionEvent.ACTION_CANCEL){
if(isClicked){
isClicked = false;
invalidate();
if (null != mOnClickListener && ct && event.getAction() != MotionEvent.ACTION_CANCEL) {
mOnClickListener.onClick(this);
return true;
}
}
return false;
}else if(event.getAction() == MotionEvent.ACTION_MOVE){
return isClicked;
}
return super.onTouchEvent(event);
}
private OnClickListener mOnClickListener;
public void setOnViewClickListener(OnClickListener clickListener){
mOnClickListener = clickListener;
}
//计算两点的距离
private int distance(PointF point1, PointF point2) {
int disX = (int) Math.abs(point1.x - point2.x);
int disY = (int) Math.abs(point1.y - point2.y);
return (int) Math.sqrt(Math.pow(disX, 2) + Math.pow(disY, 2));
}
//将sp值转换为px值,保证文字大小不变
public static int sp2px(Context context, float spValue) {
final float fontScale = context.getResources().getDisplayMetrics().scaledDensity;
return (int) (spValue * fontScale + 0.5f);
}
}
以上三种方式的完整代码:https://github.com/gengqifu/Irregular-View