最近项目里面遇到了需要实现圆角控件的需求,网上百度发现并没有找到一个特别简单的方法,大都是通过Paint.setXfermode来实现的,如果只想快速实现效果,直接点击https://github.com/ShadowWalkerGIT/RoundView 那么Paint.setXfermode是用来干嘛的呢,我们看看源码注释
/**
* Set or clear the transfer mode object. A transfer mode defines how
* source pixels (generate by a drawing command) are composited with
* the destination pixels (content of the render target).
* <p />
* Pass null to clear any previous transfer mode.
* As a convenience, the parameter passed is also returned.
* <p />
* {@link PorterDuffXfermode} is the most common transfer mode.
*
* @param xfermode May be null. The xfermode to be installed in the paint
* @return xfermode
*/
public Xfermode setXfermode(Xfermode xfermode) {
int newMode = xfermode != null ? xfermode.porterDuffMode : Xfermode.DEFAULT;
int curMode = mXfermode != null ? mXfermode.porterDuffMode : Xfermode.DEFAULT;
if (newMode != curMode) {
nSetXfermode(mNativePaint, newMode);
}
mXfermode = xfermode;
return xfermode;
}
从注释中我们看出这个方法是用来设置源图像和目标图像的混合模式的,而且最常用的模式是PorterDuffXfermode,那我们再来看看PorterDuffXfermode的源码注释
package android.graphics;
/**
* <p>Specialized implementation of {@link Paint}'s
* {@link Paint#setXfermode(Xfermode) transfer mode}. Refer to the
* documentation of the {@link PorterDuff.Mode} enum for more
* information on the available alpha compositing and blending modes.</p>
*/
public class PorterDuffXfermode extends Xfermode {
/**
* Create an xfermode that uses the specified porter-duff mode.
*
* @param mode The porter-duff mode that is applied
*/
public PorterDuffXfermode(PorterDuff.Mode mode) {
porterDuffMode = mode.nativeInt;
}
}
这里我们发现源码注释写的非常简单,让我们去PorterDuff.Mode里面查看,这里我们仅仅贴出PorterDuff.Mode里面的几个常量,注释就不贴了,感兴趣的同学自行查阅源码
public static Mode intToMode(int val) {
switch (val) {
default:
case 0: return Mode.CLEAR;
case 1: return Mode.SRC;
case 2: return Mode.DST;
case 3: return Mode.SRC_OVER;
case 4: return Mode.DST_OVER;
case 5: return Mode.SRC_IN;
case 6: return Mode.DST_IN;
case 7: return Mode.SRC_OUT;
case 8: return Mode.DST_OUT;
case 9: return Mode.SRC_ATOP;
case 10: return Mode.DST_ATOP;
case 11: return Mode.XOR;
case 16: return Mode.DARKEN;
case 17: return Mode.LIGHTEN;
case 13: return Mode.MULTIPLY;
case 14: return Mode.SCREEN;
case 12: return Mode.ADD;
case 15: return Mode.OVERLAY;
}
}
那么这些常量都是什么意思呢,这里我就不一一解释了,给大家看一张图
接下来我们看看具体实现,直接附上源码
package com.sw.roundviewdemo;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.graphics.RectF;
import android.support.annotation.Nullable;
import android.support.v7.widget.AppCompatImageView;
import android.util.AttributeSet;
public class RoundImageView extends AppCompatImageView {
private final RectF roundRect = new RectF();
private float mRadius = 10;
private final Paint maskPaint = new Paint();
private final Paint zonePaint = new Paint();
private boolean isOval = true;
public RoundImageView(Context context) {
this(context, null);
}
public RoundImageView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public RoundImageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
maskPaint.setAntiAlias(true);//设置抗锯齿
maskPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
zonePaint.setAntiAlias(true);
float density = getResources().getDisplayMetrics().density;
mRadius *= density;
}
public void setRadius(float radius) {
this.mRadius = radius;
invalidate();
}
/**
* 是否是圆形
*
* @param isOval
*/
public void setOval(boolean isOval) {
this.isOval = isOval;
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
int width = getWidth();
int height = getHeight();
if (!isOval) {//圆角ImageView
roundRect.set(0, 0, width, height);
} else {//圆形ImageView
if (width > height) {
left = width / 2 - height / 2;
right = left + height;
top = 0;
bottom = height;
} else {
top = height / 2 - width / 2;
bottom = top + width;
left = 0;
right = width;
}
roundRect.set(left, top, right, bottom);
}
}
@Override
public void draw(Canvas canvas) {
canvas.saveLayer(roundRect, zonePaint, Canvas.ALL_SAVE_FLAG);//保存现有的内容到一个offscreen bitmap中
if (!isOval) {
canvas.drawRoundRect(roundRect, mRadius, mRadius, zonePaint);//绘制一个与现在图片宽高一样的圆角矩形
} else {
canvas.drawOval(roundRect, zonePaint);
}
canvas.saveLayer(roundRect, maskPaint, Canvas.ALL_SAVE_FLAG);//保存叠加后的内容
super.draw(canvas);
canvas.restore();//清空所有的图像矩阵修改状态
}
}
但是,这种方式实现效率比较低,原因就在于draw方法里面调用了canvas.saveLayer(),调用这个方法的时候需要两倍以上的渲染成本,应尽量避免使用此方法,我们来看看这个方法在源码里面的注释
/**
* This behaves the same as save(), but in addition it allocates and
* redirects drawing to an offscreen bitmap.
* <p class="note"><strong>Note:</strong> this method is very expensive,
* incurring more than double rendering cost for contained content. Avoid
* using this method, especially if the bounds provided are large, or if
* the {@link #CLIP_TO_LAYER_SAVE_FLAG} is omitted from the
* {@code saveFlags} parameter. It is recommended to use a
* {@link android.view.View#LAYER_TYPE_HARDWARE hardware layer} on a View
* to apply an xfermode, color filter, or alpha, as it will perform much
* better than this method.
* <p>
* All drawing calls are directed to a newly allocated offscreen bitmap.
* Only when the balancing call to restore() is made, is that offscreen
* buffer drawn back to the current target of the Canvas (either the
* screen, it's target Bitmap, or the previous layer).
* <p>
* Attributes of the Paint - {@link Paint#getAlpha() alpha},
* {@link Paint#getXfermode() Xfermode}, and
* {@link Paint#getColorFilter() ColorFilter} are applied when the
* offscreen bitmap is drawn back when restore() is called.
*
* @deprecated Use {@link #saveLayer(RectF, Paint)} instead.
* @param bounds May be null. The maximum size the offscreen bitmap
* needs to be (in local coordinates)
* @param paint This is copied, and is applied to the offscreen when
* restore() is called.
* @param saveFlags see _SAVE_FLAG constants, generally {@link #ALL_SAVE_FLAG} is recommended
* for performance reasons.
* @return value to pass to restoreToCount() to balance this save()
*/
public int saveLayer(@Nullable RectF bounds, @Nullable Paint paint, @Saveflags int saveFlags) {
if (bounds == null) {
bounds = new RectF(getClipBounds());
}
return saveLayer(bounds.left, bounds.top, bounds.right, bounds.bottom, paint, saveFlags);
}
接下来我们要介绍另外一种方法,就是通过给View设置outline的形式,主要是调用View的setOutlineProvider方法,那么这个方法是干嘛的呢,我们看看源码注释
/**
* Sets the {@link ViewOutlineProvider} of the view, which generates the Outline that defines
* the shape of the shadow it casts, and enables outline clipping.
* <p>
* The default ViewOutlineProvider, {@link ViewOutlineProvider#BACKGROUND}, queries the Outline
* from the View's background drawable, via {@link Drawable#getOutline(Outline)}. Changing the
* outline provider with this method allows this behavior to be overridden.
* <p>
* If the ViewOutlineProvider is null, if querying it for an outline returns false,
* or if the produced Outline is {@link Outline#isEmpty()}, shadows will not be cast.
* <p>
* Only outlines that return true from {@link Outline#canClip()} may be used for clipping.
*
* @see #setClipToOutline(boolean)
* @see #getClipToOutline()
* @see #getOutlineProvider()
*/
public void setOutlineProvider(ViewOutlineProvider provider) {
mOutlineProvider = provider;
invalidateOutline();
}
这个方法是用来给View设置ViewOutlineProvider的,这个ViewOutlineProvider是用来生成该View投影的形状。也就是说通过该ViewoutlineProvider来实现我们想要的圆形或者圆角的展示效果。具体怎么做呢,其实也很简单,具体调用就两行代码,我们一起来看看
public class ViewStyleSetter {
private View mView;
public ViewStyleSetter(View view) {
this.mView = view;
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public void setRound(float radius) {
this.mView.setClipToOutline(true);//用outline裁剪内容区域
this.mView.setOutlineProvider(new RoundViewOutlineProvider(radius));
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public void setOval() {
this.mView.setClipToOutline(true);//用outline裁剪内容区域
this.mView.setOutlineProvider(new OvalViewOutlineProvider());
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public void clearShapeStyle() {
this.mView.setClipToOutline(false);
}
}
这个是自定义的一个辅助类,里面有两个主要的方法,setRound是用来实现圆角View的,setOval是用来实现圆形View的,我们先来看看圆角View所用到的RoundViewOutlineProvider
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public class RoundViewOutlineProvider extends ViewOutlineProvider {
private float mRadius;//圆角弧度
public RoundViewOutlineProvider(float radius) {
this.mRadius = radius;
}
@Override
public void getOutline(View view, Outline outline) {
Rect rect = new Rect();
view.getGlobalVisibleRect(rect);//将view的区域保存在rect中
Rect selfRect = new Rect(0, 0, rect.right - rect.left, rect.bottom - rect.top);//绘制区域
outline.setRoundRect(selfRect, mRadius);
}
}
主要就是在getOutline方法里面设置了Rect的区域,然后调用了outline.setRoundRect方法来实现圆角outline,Rect坐标lef,top,right,bottom分别对应0,0,0,rect宽,rect高
接下来我们来看看圆形的ViewOutlineProvider
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public class OvalViewOutlineProvider extends ViewOutlineProvider {
public OvalViewOutlineProvider() {
}
@Override
public void getOutline(final View view, final Outline outline) {
Rect selfRect;
Rect rect = new Rect();
view.getGlobalVisibleRect(rect);
selfRect = getOvalRect(rect);
outline.setOval(selfRect);
}
/**
* 以矩形的中心点为圆心,较短的边为直径画圆
*
* @param rect
* @return
*/
private Rect getOvalRect(Rect rect) {
int width = rect.right - rect.left;
int height = rect.bottom - rect.top;
int left, top, right, bottom;
int dW = width / 2;
int dH = height / 2;
if (width > height) {
left = dW - dH;
top = 0;
right = dW + dH;
bottom = dH * 2;
} else {
left = dH - dW;
top = 0;
right = dH + dW;
bottom = dW * 2;
}
return new Rect(left, top, right, bottom);
}
}
主要就是调用outline.setOval来实现圆形的轮廓,圆形Rect坐标计算代码里面也注释了,简单的说就是"以矩阵的中心点为圆心,较短的边为直径画圆"
那么具体怎么使用呢,很简单,只需要通过想要实现圆角效果的View生成一个ViewStyleSetter,然后调用ViewStyleSetter的setRound setOval方法即可
View fl_main = findViewById(R.id.fl_main);
ViewStyleSetter viewStyleSetter = new ViewStyleSetter(fl_main);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
viewStyleSetter.setRound(10);//实现圆角效果
viewStyleSetter.setOval();//实现圆形效果
}
目前支持View ViewGroup,源码链接 https://github.com/ShadowWalkerGIT/RoundView