一、问题描述
在博客
- 【我的Android进阶之旅】实现一个四周圆角并且中间有小圆来进行分隔的自定义LinearLayout
- https://ouyangpeng.blog.csdn.net/article/details/85295859
里面我绘制了一个自定义View,如下图所示:
但是,由于使用了PorterDuffXfermode来处理两个图层的图层混合模式,而网上的资料说
- https://blog.csdn.net/wingichoy/article/details/50534175
- https://www.jianshu.com/p/78c36742d50f
都说在使用Xfermode时,为了保险起见,都要禁用硬件加速。
因此我就将禁用硬件加速的代码加入到了自定义View的代码中
//禁用硬件加速
setLayerType(View.LAYER_TYPE_SOFTWARE, null);
如下所示:
@Override
protected void onDraw(Canvas canvas) {
//制成一个白色的圆角矩形 作为背景 画2个半圆 和 18个小圆 作为分隔线
initWidthAndHeight();
//禁用硬件加速
setLayerType(View.LAYER_TYPE_SOFTWARE, null);
//使用离屏绘制 存为新图层
int layerID = canvas.saveLayer(0, 0, mWidth, mHeight, backGroundPaint, Canvas.ALL_SAVE_FLAG);
// 绘制目标图
drawLeftAndRightBigHalfCircle(canvas);
// 设置图层混合模式
backGroundPaint.setXfermode(mXfermode);
// 绘制源图
drawBackGroundAndSmallCircle(canvas);
// 清除混合模式
backGroundPaint.setXfermode(null);
// 恢复保存的图层;
canvas.restoreToCount(layerID);
// ========================== 第四步、 让RelativeLayout绘制自己
super.onDraw(canvas);
}
完整的代码如下:
package com.oyp.view;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.res.TypedArray;
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.util.AttributeSet;
import android.view.View;
import android.widget.LinearLayout;
public class CustomOypRoundLinearLayout extends LinearLayout {
/**
* 大圆、小圆的圆心的Y坐标
*/
private int circleStartY;
/**
* 第一个小圆的圆心的X坐标
*/
private int smallCircleStartX;
/**
* 小圆的个数
*/
private int smallCircleCount;
/**
* 小圆的半径
*/
private int smallCircleRadius;
/**
* 小圆之间的间距
*/
private int smallCircleMargin;
/**
* 大圆的半径
*/
private int bigCircleRadius;
/**
* 背景的圆角
*/
private int backgroundRadius;
/**
* 背景的背景颜色
*/
private int backgroundColor;
/**
* 小圆的颜色
*/
private int smallCircleColor;
/**
* 大圆的颜色
*/
private int bigCircleColor;
private RectF backGroundRectF;
private Paint backGroundPaint;
private Paint circlePaint;
private RectF leftOval;
private RectF rightOval;
private int mWidth;
private int mHeight;
private PorterDuffXfermode mXfermode;
public CustomOypRoundLinearLayout(Context context) {
this(context, null);
}
public CustomOypRoundLinearLayout(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public CustomOypRoundLinearLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context, attrs);
}
@TargetApi(21)
public CustomOypRoundLinearLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init(context, attrs);
}
private void init(Context context, @Nullable AttributeSet attrs) {
//初始化自定义属性
initTypeArray(context, attrs);
//想要重写onDraw,就要调用setWillNotDraw(false)
//ViewGroup默认情况下,出于性能考虑,会被设置成WILL_NOT_DROW,这样,ondraw就不会被执行了。
// 如果我们想重写一个viewgroup的ondraw方法,有两种方法:
// 1,构造函数中,给viewgroup设置一个颜色。
// 2,构造函数中,调用setWillNotDraw(false),去掉其WILL_NOT_DRAW flag。
// 在viewgroup初始化的时候,它调用了一个私有方法:initViewGroup,它里面会有一句setFlags(WILLL_NOT_DRAW,DRAW_MASK);
// 相当于调用了setWillNotDraw(true),所以说,对于ViewGroup,他就认为是透明的了,
// 如果我们想要重写onDraw,就要调用setWillNotDraw(false)
setWillNotDraw(false);
initPaint();
initXfermode();
initRectF();
}
private void initPaint() {
backGroundPaint = new Paint();
backGroundPaint.setColor(backgroundColor);
circlePaint = new Paint();
circlePaint.setStyle(Paint.Style.FILL);
circlePaint.setColor(bigCircleColor);
}
private void initXfermode() {
//禁用硬件加速 https://www.jianshu.com/p/78c36742d50f
//从硬件加速不支持的函数列表中,我们可以看到AvoidXfermode,PixelXorXfermode是完全不支持的,而PorterDuffXfermode是部分不支持的。
//如果你的APP跑在API 14版本以后,而你洽好要用那些不支持硬件加速的函数要怎么办? 那就只好禁用硬件加速喽
//该方法千万别放到onDraw()方法里面调用,否则会不停的重绘的,因为该方法调用了invalidate() 方法
setLayerType(View.LAYER_TYPE_SOFTWARE, null);
mXfermode = new PorterDuffXfermode(PorterDuff.Mode.XOR);
}
private void initRectF() {
backGroundRectF = new RectF();
leftOval = new RectF();
rightOval = new RectF();
}
private void initWidthAndHeight() {
mWidth = getWidth();
mHeight = getHeight();
}
private void initTypeArray(Context context, AttributeSet attrs) {
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CustomOypRoundLinearLayout);
//个数
smallCircleCount = typedArray.getInteger(R.styleable.CustomOypRoundLinearLayout_smallCircleCount,
getResources().getInteger(R.integer.smallCircleCount));
//尺寸
circleStartY = typedArray.getDimensionPixelOffset(R.styleable.CustomOypRoundLinearLayout_circleStartY,
getResources().getDimensionPixelOffset(R.dimen.circleStartY));
smallCircleStartX = typedArray.getDimensionPixelOffset(R.styleable.CustomOypRoundLinearLayout_smallCircleStartX,
getResources().getDimensionPixelOffset(R.dimen.smallCircleStartX));
smallCircleRadius = typedArray.getDimensionPixelOffset(R.styleable.CustomOypRoundLinearLayout_smallCircleRadius,
getResources().getDimensionPixelOffset(R.dimen.smallCircleRadius));
bigCircleRadius = typedArray.getDimensionPixelOffset(R.styleable.CustomOypRoundLinearLayout_bigCircleRadius,
getResources().getDimensionPixelOffset(R.dimen.bigCircleRadius));
backgroundRadius = typedArray.getDimensionPixelOffset(R.styleable.CustomOypRoundLinearLayout_backgroundRadius,
getResources().getDimensionPixelOffset(R.dimen.backgroundRadius));
//颜色
backgroundColor = typedArray.getColor(R.styleable.CustomOypRoundLinearLayout_backgroundColor,
getResources().getColor(R.color.backgroundColor));
smallCircleColor = typedArray.getColor(R.styleable.CustomOypRoundLinearLayout_smallCircleColor,
getResources().getColor(R.color.smallCircleColor));
bigCircleColor = typedArray.getColor(R.styleable.CustomOypRoundLinearLayout_bigCircleColor,
getResources().getColor(R.color.bigCircleColor));
//回收typedArray
typedArray.recycle();
}
@Override
protected void onDraw(Canvas canvas) {
//制成一个白色的圆角矩形 作为背景 画2个半圆 和 18个小圆 作为分隔线
initWidthAndHeight();
//使用离屏绘制 存为新图层
int layerID = canvas.saveLayer(0, 0, mWidth, mHeight, backGroundPaint, Canvas.ALL_SAVE_FLAG);
// 绘制目标图
drawLeftAndRightBigHalfCircle(canvas);
// 设置图层混合模式
backGroundPaint.setXfermode(mXfermode);
// 绘制源图
drawBackGroundAndSmallCircle(canvas);
// 清除混合模式
backGroundPaint.setXfermode(null);
// 恢复保存的图层;
canvas.restoreToCount(layerID);
// ========================== 第四步、 让RelativeLayout绘制自己
super.onDraw(canvas);
}
private void drawBackGroundAndSmallCircle(Canvas canvas) {
// ========================== 绘制白色矩形
backGroundRectF.set(0, 0, mWidth, mHeight);
canvas.drawRoundRect(backGroundRectF, backgroundRadius, backgroundRadius, backGroundPaint);
// ========================== 绘制19个小圆 作为分隔线
// 最后一个小圆和第一个小圆之间 有18段分隔空白
smallCircleMargin = ((mWidth - smallCircleStartX) - (smallCircleStartX)) / (smallCircleCount - 1);
circlePaint.setColor(smallCircleColor);
for (int i = 0; i < smallCircleCount; i++) {
canvas.drawCircle(smallCircleStartX + i * smallCircleMargin, circleStartY, smallCircleRadius, circlePaint);
}
}
private void drawLeftAndRightBigHalfCircle(Canvas canvas) {
// ========================== 绘制2个半圆
leftOval.set(-bigCircleRadius, circleStartY - bigCircleRadius,
bigCircleRadius, circleStartY + bigCircleRadius);
/*
* drawArc(RectF oval, float startAngle, float sweepAngle, boolean useCenter, Paint paint)
* oval :指定圆弧的外轮廓矩形区域。
* startAngle: 圆弧起始角度,单位为度。
* sweepAngle: 圆弧扫过的角度,顺时针方向,单位为度,从右中间开始为零度。
* useCenter: 如果为True时,在绘制圆弧时将圆心包括在内,通常用来绘制扇形。关键是这个变量,下面将会详细介绍。
* paint: 绘制圆弧的画板属性,如颜色,是否填充等。
*/
//左边的半圆
canvas.drawArc(leftOval, -90, 180, true, circlePaint);
//右边的半圆
rightOval.set(mWidth - bigCircleRadius, circleStartY - bigCircleRadius,
mWidth + bigCircleRadius, circleStartY + bigCircleRadius);
canvas.drawArc(rightOval, 90, 180, true, circlePaint);
}
}
1.1 问题来了,不停地重绘
1.2 分析原因
/**
* <p>Specifies the type of layer backing this view. The layer can be
* {@link #LAYER_TYPE_NONE}, {@link #LAYER_TYPE_SOFTWARE} or
* {@link #LAYER_TYPE_HARDWARE}.</p>
*
* <p>A layer is associated with an optional {@link android.graphics.Paint}
* instance that controls how the layer is composed on screen. The following
* properties of the paint are taken into account when composing the layer:</p>
* <ul>
* <li>{@link android.graphics.Paint#getAlpha() Translucency (alpha)}</li>
* <li>{@link android.graphics.Paint#getXfermode() Blending mode}</li>
* <li>{@link android.graphics.Paint#getColorFilter() Color filter}</li>
* </ul>
*
* <p>If this view has an alpha value set to < 1.0 by calling
* {@link #setAlpha(float)}, the alpha value of the layer's paint is superseded
* by this view's alpha value.</p>
*
* <p>Refer to the documentation of {@link #LAYER_TYPE_NONE},
* {@link #LAYER_TYPE_SOFTWARE} and {@link #LAYER_TYPE_HARDWARE}
* for more information on when and how to use layers.</p>
*
* @param layerType The type of layer to use with this view, must be one of
* {@link #LAYER_TYPE_NONE}, {@link #LAYER_TYPE_SOFTWARE} or
* {@link #LAYER_TYPE_HARDWARE}
* @param paint The paint used to compose the layer. This argument is optional
* and can be null. It is ignored when the layer type is
* {@link #LAYER_TYPE_NONE}
*
* @see #getLayerType()
* @see #LAYER_TYPE_NONE
* @see #LAYER_TYPE_SOFTWARE
* @see #LAYER_TYPE_HARDWARE
* @see #setAlpha(float)
*
* @attr ref android.R.styleable#View_layerType
*/
public void setLayerType(int layerType, @Nullable Paint paint) {
if (layerType < LAYER_TYPE_NONE || layerType > LAYER_TYPE_HARDWARE) {
throw new IllegalArgumentException("Layer type can only be one of: LAYER_TYPE_NONE, "
+ "LAYER_TYPE_SOFTWARE or LAYER_TYPE_HARDWARE");
}
boolean typeChanged = mRenderNode.setLayerType(layerType);
if (!typeChanged) {
setLayerPaint(paint);
return;
}
if (layerType != LAYER_TYPE_SOFTWARE) {
// Destroy any previous software drawing cache if present
// NOTE: even if previous layer type is HW, we do this to ensure we've cleaned up
// drawing cache created in View#draw when drawing to a SW canvas.
destroyDrawingCache();
}
mLayerType = layerType;
mLayerPaint = mLayerType == LAYER_TYPE_NONE ? null : paint;
mRenderNode.setLayerPaint(mLayerPaint);
// draw() behaves differently if we are on a layer, so we need to
// invalidate() here
invalidateParentCaches();
invalidate(true);
}
android.view.View#setLayerType 方法调用了 android.view.View#invalidate(boolean) 方法,所以导致了重绘
/**
* Invalidate the whole view. If the view is visible,
* {@link #onDraw(android.graphics.Canvas)} will be called at some point in
* the future.
* <p>
* This must be called from a UI thread. To call from a non-UI thread, call
* {@link #postInvalidate()}.
*/
public void invalidate() {
invalidate(true);
}
/**
* This is where the invalidate() work actually happens. A full invalidate()
* causes the drawing cache to be invalidated, but this function can be
* called with invalidateCache set to false to skip that invalidation step
* for cases that do not need it (for example, a component that remains at
* the same dimensions with the same content).
*
* @param invalidateCache Whether the drawing cache for this view should be
* invalidated as well. This is usually true for a full
* invalidate, but may be set to false if the View's contents or
* dimensions have not changed.
* @hide
*/
public void invalidate(boolean invalidateCache) {
invalidateInternal(0, 0, mRight - mLeft, mBottom - mTop, invalidateCache, true);
}
所以原因就是
在onDraw()方法中调用了android.view.View#setLayerType(int layerType, @Nullable Paint paint)** 方法
.而 android.view.View#setLayerType(int layerType, @Nullable Paint paint) 方法调用了 android.view.View#invalidate(boolean invalidateCache) 方法,
所以导致了不停地重绘
1.3 解决这个坑
将setLayerType(View.LAYER_TYPE_SOFTWARE, null);
放到初始化的地方去调用,只调用一次。
完整代码如下
package com.oyp.view;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.res.TypedArray;
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.util.AttributeSet;
import android.view.View;
import android.widget.LinearLayout;
public class CustomOypRoundLinearLayout extends LinearLayout {
/**
* 大圆、小圆的圆心的Y坐标
*/
private int circleStartY;
/**
* 第一个小圆的圆心的X坐标
*/
private int smallCircleStartX;
/**
* 小圆的个数
*/
private int smallCircleCount;
/**
* 小圆的半径
*/
private int smallCircleRadius;
/**
* 小圆之间的间距
*/
private int smallCircleMargin;
/**
* 大圆的半径
*/
private int bigCircleRadius;
/**
* 背景的圆角
*/
private int backgroundRadius;
/**
* 背景的背景颜色
*/
private int backgroundColor;
/**
* 小圆的颜色
*/
private int smallCircleColor;
/**
* 大圆的颜色
*/
private int bigCircleColor;
private RectF backGroundRectF;
private Paint backGroundPaint;
private Paint circlePaint;
private RectF leftOval;
private RectF rightOval;
private int mWidth;
private int mHeight;
private PorterDuffXfermode mXfermode;
public CustomOypRoundLinearLayout(Context context) {
this(context, null);
}
public CustomOypRoundLinearLayout(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public CustomOypRoundLinearLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context, attrs);
}
@TargetApi(21)
public CustomOypRoundLinearLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init(context, attrs);
}
private void init(Context context, @Nullable AttributeSet attrs) {
//初始化自定义属性
initTypeArray(context, attrs);
//想要重写onDraw,就要调用setWillNotDraw(false)
//ViewGroup默认情况下,出于性能考虑,会被设置成WILL_NOT_DROW,这样,ondraw就不会被执行了。
// 如果我们想重写一个viewgroup的ondraw方法,有两种方法:
// 1,构造函数中,给viewgroup设置一个颜色。
// 2,构造函数中,调用setWillNotDraw(false),去掉其WILL_NOT_DRAW flag。
// 在viewgroup初始化的时候,它调用了一个私有方法:initViewGroup,它里面会有一句setFlags(WILLL_NOT_DRAW,DRAW_MASK);
// 相当于调用了setWillNotDraw(true),所以说,对于ViewGroup,他就认为是透明的了,
// 如果我们想要重写onDraw,就要调用setWillNotDraw(false)
setWillNotDraw(false);
initPaint();
initXfermode();
initRectF();
}
private void initPaint() {
backGroundPaint = new Paint();
backGroundPaint.setColor(backgroundColor);
circlePaint = new Paint();
circlePaint.setStyle(Paint.Style.FILL);
circlePaint.setColor(bigCircleColor);
}
private void initXfermode() {
//禁用硬件加速 https://www.jianshu.com/p/78c36742d50f
//从硬件加速不支持的函数列表中,我们可以看到AvoidXfermode,PixelXorXfermode是完全不支持的,而PorterDuffXfermode是部分不支持的。
//如果你的APP跑在API 14版本以后,而你洽好要用那些不支持硬件加速的函数要怎么办? 那就只好禁用硬件加速喽
//该方法千万别放到onDraw()方法里面调用,否则会不停的重绘的,因为该方法调用了invalidate() 方法
setLayerType(View.LAYER_TYPE_SOFTWARE, null);
mXfermode = new PorterDuffXfermode(PorterDuff.Mode.XOR);
}
private void initRectF() {
backGroundRectF = new RectF();
leftOval = new RectF();
rightOval = new RectF();
}
private void initWidthAndHeight() {
mWidth = getWidth();
mHeight = getHeight();
}
private void initTypeArray(Context context, AttributeSet attrs) {
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CustomOypRoundLinearLayout);
//个数
smallCircleCount = typedArray.getInteger(R.styleable.CustomOypRoundLinearLayout_smallCircleCount,
getResources().getInteger(R.integer.smallCircleCount));
//尺寸
circleStartY = typedArray.getDimensionPixelOffset(R.styleable.CustomOypRoundLinearLayout_circleStartY,
getResources().getDimensionPixelOffset(R.dimen.circleStartY));
smallCircleStartX = typedArray.getDimensionPixelOffset(R.styleable.CustomOypRoundLinearLayout_smallCircleStartX,
getResources().getDimensionPixelOffset(R.dimen.smallCircleStartX));
smallCircleRadius = typedArray.getDimensionPixelOffset(R.styleable.CustomOypRoundLinearLayout_smallCircleRadius,
getResources().getDimensionPixelOffset(R.dimen.smallCircleRadius));
bigCircleRadius = typedArray.getDimensionPixelOffset(R.styleable.CustomOypRoundLinearLayout_bigCircleRadius,
getResources().getDimensionPixelOffset(R.dimen.bigCircleRadius));
backgroundRadius = typedArray.getDimensionPixelOffset(R.styleable.CustomOypRoundLinearLayout_backgroundRadius,
getResources().getDimensionPixelOffset(R.dimen.backgroundRadius));
//颜色
backgroundColor = typedArray.getColor(R.styleable.CustomOypRoundLinearLayout_backgroundColor,
getResources().getColor(R.color.backgroundColor));
smallCircleColor = typedArray.getColor(R.styleable.CustomOypRoundLinearLayout_smallCircleColor,
getResources().getColor(R.color.smallCircleColor));
bigCircleColor = typedArray.getColor(R.styleable.CustomOypRoundLinearLayout_bigCircleColor,
getResources().getColor(R.color.bigCircleColor));
//回收typedArray
typedArray.recycle();
}
@Override
protected void onDraw(Canvas canvas) {
//制成一个白色的圆角矩形 作为背景 画2个半圆 和 18个小圆 作为分隔线
initWidthAndHeight();
//使用离屏绘制 存为新图层
int layerID = canvas.saveLayer(0, 0, mWidth, mHeight, backGroundPaint, Canvas.ALL_SAVE_FLAG);
// 绘制目标图
drawLeftAndRightBigHalfCircle(canvas);
// 设置图层混合模式
backGroundPaint.setXfermode(mXfermode);
// 绘制源图
drawBackGroundAndSmallCircle(canvas);
// 清除混合模式
backGroundPaint.setXfermode(null);
// 恢复保存的图层;
canvas.restoreToCount(layerID);
// ========================== 第四步、 让RelativeLayout绘制自己
super.onDraw(canvas);
}
private void drawBackGroundAndSmallCircle(Canvas canvas) {
// ========================== 绘制白色矩形
backGroundRectF.set(0, 0, mWidth, mHeight);
canvas.drawRoundRect(backGroundRectF, backgroundRadius, backgroundRadius, backGroundPaint);
// ========================== 绘制19个小圆 作为分隔线
// 最后一个小圆和第一个小圆之间 有18段分隔空白
smallCircleMargin = ((mWidth - smallCircleStartX) - (smallCircleStartX)) / (smallCircleCount - 1);
circlePaint.setColor(smallCircleColor);
for (int i = 0; i < smallCircleCount; i++) {
canvas.drawCircle(smallCircleStartX + i * smallCircleMargin, circleStartY, smallCircleRadius, circlePaint);
}
}
private void drawLeftAndRightBigHalfCircle(Canvas canvas) {
// ========================== 绘制2个半圆
leftOval.set(-bigCircleRadius, circleStartY - bigCircleRadius,
bigCircleRadius, circleStartY + bigCircleRadius);
/*
* drawArc(RectF oval, float startAngle, float sweepAngle, boolean useCenter, Paint paint)
* oval :指定圆弧的外轮廓矩形区域。
* startAngle: 圆弧起始角度,单位为度。
* sweepAngle: 圆弧扫过的角度,顺时针方向,单位为度,从右中间开始为零度。
* useCenter: 如果为True时,在绘制圆弧时将圆心包括在内,通常用来绘制扇形。关键是这个变量,下面将会详细介绍。
* paint: 绘制圆弧的画板属性,如颜色,是否填充等。
*/
//左边的半圆
canvas.drawArc(leftOval, -90, 180, true, circlePaint);
//右边的半圆
rightOval.set(mWidth - bigCircleRadius, circleStartY - bigCircleRadius,
mWidth + bigCircleRadius, circleStartY + bigCircleRadius);
canvas.drawArc(rightOval, 90, 180, true, circlePaint);
}
}
继续查看下GPU的信息,如下所示
只绘制了一次,然后不再绘制,解决了这个性能问题。
作者:欧阳鹏 欢迎转载,与人分享是进步的源泉!
转载请保留原文地址:https://blog.csdn.net/qq446282412/article/details/88998767
☞ 本人QQ: 3024665621
☞ QQ交流群: 123133153
☞ github.com/ouyangpeng
☞ [email protected]