赞
踩
目录:
Xfermode是什么?
在Android绘制中,通过使用Xfermode将绘制的图形的像素和Canvas上对应位置的像素按照一定的规则进行混合,形成新的像素,再更新到Canvas中形成最终的图形。
像素组成的4元素:ARGB
我们一个像素的颜色都是由四个分量组成,即ARGB,A表示的是我们Alpha值,RGB表示的是颜色
S表示的是源像素,源像素的值表示[Sa,Sc] Sa表示的就是源像素的Alpha值,Sc表示源像素的颜色值
D表示的是目标像素,目标像素的值表示[Da,Dc] Da表示的就是目标像素的Alpha值
Xfermode有三个子类:AvoidXfermode, PixelXorXfermode和PorterDuffXfermode。其中AvoidXfermode, PixelXorXfermode已经过时不推荐使用。那么PorterDuffXfermode则是需要了解的东西。
2. Xfermode理解起来并不是很难,根据上面的图可以理解为,两个不同的像素点。通过Xfermode的不同的混合模式混合之后展示出来的新的像素点效果。(注意这里是针对每一个像素的混合效果。而且这两个像素点需要是在画布上的同一位置,可以理解为重叠)
// 初始化PorterDuffXfermode
private PorterDuffXfermode xfermode = new PorterDuffXfermode(PorterDuff.Mode.SRC_IN);
// 在ondraw中使用PorterDuffXfermode
protected void onDraw(Canvas canvas) {
// DstBitmap SRCBitmap 为两个不同的bitmap
canvas.drawBitmap(SrcBitmap,0,0,mPaint);
// PorterDuffXfermode和paint联用
mPaint.setXfermode(xfermode);
canvas.drawBitmap(DstBitmap,0,0,mPaint);
// 将xfermode制空
mPaint.setXfermode(null);
}
以上的代码也比较简单理解: 先draw一个bitmap,然后设置paint的xfermode,然后在画第二个bitmap。这样他们重叠的部分就会出现不通过的UI效果了。
官方的贴图非常形象的展示出各种混合模式使用后展示的效果。
接下来挑出一个常用的例子SRC_IN来解释下这些算法的基本应用。
圆形头像实现的方式可能有很多。比如用bitmapshader等等。使用xfermode同样能实现。
SRC_IN (5),
/** [Sa * Da, Sa * Dc] */
SRC_IN的算法是这样的:
(a)Sa * Da:源图(S)像素透明度和目标图片(D)像素的透明的决定混合后像素的透明度
(b)Sa * Dc:源图(S)像素透明度和目标图片(D)像素的颜色决定混合后像素的颜色
那么混合的图解:
从(a)(b)可以看出,源图片只采用了透明度的变化。混合后图像像素的透明度和颜色都和源图的像素的透明度的有关。如果源图的像素是透明的,那么混合后的像素为透明。反之不透明。所以源图为:
从(b)可以看出,决定混合后图像素颜色是由目标图片(D)决定的。所以目标图片是:
这里主要是理解算法:[Sa * Da, Sa * Dc]
示例代码:
package com.hdp.testvie.xformode; import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.PorterDuff; import android.graphics.PorterDuffXfermode; import android.util.AttributeSet; import android.view.View; import androidx.annotation.Nullable; /** * 使用xfermode中的src_in模式叠加做头像。 */ public class CustomHeadView extends View { private Bitmap DBitmap; private Bitmap SBitmap; private Paint mPaint; private PorterDuffXfermode xfermode; public CustomHeadView(Context context) { this(context, null); } public CustomHeadView(Context context, @Nullable AttributeSet attrs) { this(context, attrs, 0); } public CustomHeadView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } private void init() { mPaint = new Paint(); xfermode = new PorterDuffXfermode(PorterDuff.Mode.SRC_IN); } public void drawHead(int src, int dst) { SBitmap = BitmapFactory.decodeResource(getResources(), src, null); DBitmap = BitmapFactory.decodeResource(getResources(), dst, null); invalidate(); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); setMeasuredDimension(SBitmap.getWidth(), SBitmap.getHeight()); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); int layerId = canvas.saveLayer(0, 0, getWidth(), getHeight(), null, Canvas.ALL_SAVE_FLAG); canvas.drawBitmap(SBitmap, 0, 0, mPaint); mPaint.setXfermode(xfermode); canvas.drawBitmap(DBitmap, 0, 0, mPaint); mPaint.setXfermode(null); canvas.restoreToCount(layerId); } }
上述例子已经非常清晰的说明了xfermode的算法:
源图(S)和目标图(D)像素的透明度和颜色,通过特定的算法来算出混合后新图的透明度和颜色。(注意这里是对每个像素进行操作)
使用xfermode来完成圆形头像只是其中之一。如果有特殊要求,想弄成其他的形状都是可以的。 如果我上面写的圆形图片的例子能够理解,那么其他的各种形状的例子使用的方法是一样的。
实际上实现一个效果并不是说只能采用一种叠加模式。用不同的模式也能做到相同的效果。 这里展示的刮刮卡效果,采用DST_OUT模式。
实现源码如下:
package com.hdp.testvie.xformode;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import androidx.annotation.Nullable;
import com.hdp.testvie.R;
public class GuaGuaCard extends View implements View.OnTouchListener {
/**
* 原图
*/
private Bitmap SBitmap;
/** * 目标图片 */ private Bitmap DBitmap; private Bitmap bitmap; private Paint mPaint; private PorterDuffXfermode xfermode; /** * 记录手指划过的路劲 */ private Path mPath = new Path(); private float startX; private float startY; public GuaGuaCard(Context context) { this(context, null); } public GuaGuaCard(Context context, @Nullable AttributeSet attrs) { this(context, attrs, 0); } public GuaGuaCard(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); // 去掉硬件加速 setLayerType(View.LAYER_TYPE_SOFTWARE, null); init(context); setOnTouchListener(this); } private void init(Context context) { mPaint = new Paint(); mPaint.setStyle(Paint.Style.STROKE); mPaint.setStrokeWidth(45); SBitmap = BitmapFactory.decodeResource(context.getResources(), R.drawable.one); bitmap = BitmapFactory.decodeResource(context.getResources(), R.drawable.two); DBitmap = Bitmap.createBitmap(SBitmap.getWidth(), SBitmap.getHeight(), Bitmap.Config.ARGB_8888); bitCanvas = new Canvas(DBitmap); xfermode = new PorterDuffXfermode(PorterDuff.Mode.DST_OUT); } private Canvas bitCanvas; @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); setMeasuredDimension(bitmap.getWidth(), bitmap.getHeight()); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); canvas.drawBitmap(bitmap, 0, 0, mPaint); int layerId = canvas.saveLayer(0, 0, getWidth(), getHeight(), null, Canvas.ALL_SAVE_FLAG); bitCanvas.drawPath(mPath, mPaint); canvas.drawBitmap(SBitmap, 0, 0, mPaint); // 叠加模式 mPaint.setXfermode(xfermode); canvas.drawBitmap(DBitmap, 0, 0, mPaint); // 记得重置xfermode模式 mPaint.setXfermode(null); canvas.restoreToCount(layerId); } // 移动的时候记录移动的路线 @Override public boolean onTouch(View view, MotionEvent motionEvent) { switch (motionEvent.getAction()) { case MotionEvent.ACTION_DOWN: startX = motionEvent.getX(); startY = motionEvent.getY(); mPath.moveTo(startX, startY); break; case MotionEvent.ACTION_MOVE: float endx = motionEvent.getX(); float endY = motionEvent.getY(); mPath.quadTo(startX, startY, endx, endY); startX = motionEvent.getX(); startY = motionEvent.getY(); break; case MotionEvent.ACTION_UP: break; } postInvalidate(); return true; }
}
draw方法分析:
这里使用SRC_OUT的混合模式,和上面刮刮乐相反,我们使用手指移动path生成的Bitmap作为源图,使用遮罩层作为目标图,这样混合后,源图和目标图相交的部分会变成透明,露出了底图,也就是实现了我们的效果
效果图如下:
圆角和阴影的实现方式:
这里,我们另辟蹊径,通过统一的一个父布局,通过控制子布局的位置,来进行子布局圆角和阴影的控制
圆角的实现方式:
注:这里不是给父布局添加阴影,是通过父布局的drawChild来控制子布局的阴影,这样实现的好处就是:父布局里面的任意子布局都可以很方便的添加阴影和圆角效果
作用:
源代码如下:
package com.hdp.testvie.roundview; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Path; import android.graphics.RectF; import android.util.AttributeSet; import android.view.View; import com.hdp.testvie.R; public class HLayoutParamsData { int radius; int shadowColor; int shadowDx; int shadowDy; int shadowEvaluation; RectF widgetRect; Path widgetPath; Path clipPath; boolean needClip; boolean hasShadow; public HLayoutParamsData(Context context, AttributeSet attrs) { TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.HLayout); radius = a.getDimensionPixelOffset(R.styleable.HLayout_layout_radius, 0); shadowDx = a.getDimensionPixelOffset(R.styleable.HLayout_layout_shadowDx, 0); shadowDy = a.getDimensionPixelOffset(R.styleable.HLayout_layout_shadowDy, 0); shadowColor = a.getColor(R.styleable.HLayout_layout_shadowColor, 0x99999999); shadowEvaluation = a.getDimensionPixelOffset(R.styleable.HLayout_layout_shadowEvaluation, 0); a.recycle(); needClip = radius > 0; hasShadow = shadowEvaluation > 0; } public void initPaths(View v) { widgetRect = new RectF(v.getLeft(), v.getTop(), v.getRight(), v.getBottom()); widgetPath = new Path(); clipPath = new Path(); clipPath.addRect(widgetRect, Path.Direction.CCW); clipPath.addRoundRect( widgetRect, radius, radius, Path.Direction.CW ); widgetPath.addRoundRect( widgetRect, radius, radius, Path.Direction.CW ); } }
方便我们自定义的父布局对子类参数的解析,自定义LayoutParam实现该接口
package com.hdp.testvie.roundview;
public interface HLayoutParams {
HLayoutParamsData getData();
}
现在最常用的外层布局就是约束布局,实现给子布局添加圆角和阴影
源代码如下:
package com.hdp.testvie.roundview; import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.PorterDuff; import android.graphics.PorterDuffXfermode; import android.util.AttributeSet; import android.util.Log; import android.view.View; import android.view.ViewGroup; import androidx.constraintlayout.widget.ConstraintLayout; public class HConstraintLayout extends ConstraintLayout { private Paint shadowPaint; private Paint clipPaint; public HConstraintLayout(Context context, AttributeSet attrs) { super(context, attrs); //初始化沪指阴影的paint shadowPaint = new Paint(); shadowPaint.setAntiAlias(true); shadowPaint.setDither(true); shadowPaint.setFilterBitmap(true); shadowPaint.setStyle(Paint.Style.FILL); //初始化绘制圆角的paint clipPaint = new Paint(); clipPaint.setAntiAlias(true); clipPaint.setDither(true); clipPaint.setFilterBitmap(true); clipPaint.setStyle(Paint.Style.FILL); //关闭硬件加速 setLayerType(View.LAYER_TYPE_SOFTWARE, null); } /** * 重写generateLayoutParams * @param attrs * @return */ @Override public ConstraintLayout.LayoutParams generateLayoutParams(AttributeSet attrs) { return new LayoutParams(getContext(), attrs); } @Override protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { return p instanceof LayoutParams; } /** * 布局时候,便利所有子view,初始化每个子view相关的额path,为混合成圆角和阴影做准备 * @param changed * @param left * @param top * @param right * @param bottom */ @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); for (int i = 0, size = getChildCount(); i < size; i++) { View v = getChildAt(i); ViewGroup.LayoutParams lp = v.getLayoutParams(); if (lp instanceof HLayoutParams) { HLayoutParams Hlp = (HLayoutParams) lp; Hlp.getData().initPaths(v); } } } /** * 重写drawChild方法,实现圆角和阴影 * @param canvas * @param child * @param drawingTime * @return */ @Override protected boolean drawChild(Canvas canvas, View child, long drawingTime) { ViewGroup.LayoutParams lp = child.getLayoutParams(); boolean ret = false; if (lp instanceof HLayoutParams) { HLayoutParams elp = (HLayoutParams) lp; HLayoutParamsData data = elp.getData(); if (isInEditMode()) {//预览模式采用裁剪 Log.d("TAG", "isInEditMode"); canvas.save(); canvas.clipPath(data.widgetPath); ret = super.drawChild(canvas, child, drawingTime); canvas.restore(); return ret; } if (!data.hasShadow && !data.needClip) return super.drawChild(canvas, child, drawingTime); //为解决锯齿问题,正式环境采用xfermode if (data.hasShadow) { int count = canvas.saveLayer(null, null, Canvas.ALL_SAVE_FLAG); shadowPaint.setShadowLayer(data.shadowEvaluation, data.shadowDx, data.shadowDy, data.shadowColor); shadowPaint.setColor(data.shadowColor); canvas.drawPath(data.widgetPath, shadowPaint); shadowPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT)); shadowPaint.setColor(Color.WHITE); canvas.drawPath(data.widgetPath, shadowPaint); shadowPaint.setXfermode(null); canvas.restoreToCount(count); } if (data.needClip) { Log.d("TAG", "PorterDuffXfermode CLEAR"); int count = canvas.saveLayer(child.getLeft(), child.getTop(), child.getRight(), child.getBottom(), null, Canvas.ALL_SAVE_FLAG); ret = super.drawChild(canvas, child, drawingTime); clipPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR)); clipPaint.setColor(Color.WHITE); canvas.drawPath(data.clipPath, clipPaint); clipPaint.setXfermode(null); canvas.restoreToCount(count); } } return ret; } /** * 自定义LayoutParams */ static class LayoutParams extends ConstraintLayout.LayoutParams implements HLayoutParams { private HLayoutParamsData data; public LayoutParams(Context c, AttributeSet attrs) { super(c, attrs); data = new HLayoutParamsData(c, attrs); } @Override public HLayoutParamsData getData() { return data; } } }
style文件:
<!--为了方便扩展其他layout,定义在外层,命名以layout_开头,否则lint会报红警告--> <!--为了方便扩展其他layout,定义在外层,命名以layout_开头,否则lint会报红警告--> <attr name="layout_radius" format="dimension" /> <attr name="layout_shadowColor" format="color" /> <attr name="layout_shadowEvaluation" format="dimension" /> <attr name="layout_shadowDx" format="dimension" /> <attr name="layout_shadowDy" format="dimension" /> <!--用统一一个EasyLayout,用于封装读取自定义属性--> <declare-styleable name="HLayout"> <attr name="layout_radius" /> <attr name="layout_shadowColor" /> <attr name="layout_shadowEvaluation" /> <attr name="layout_shadowDx" /> <attr name="layout_shadowDy" /> </declare-styleable> <!--和EasyLayout属性列表一样,但是命名要以XXX_Layout格式,这样开发工具会提示自定义属性--> <declare-styleable name="HConstraintLayout_Layout"> <attr name="layout_radius" /> <attr name="layout_shadowColor" /> <attr name="layout_shadowEvaluation" /> <attr name="layout_shadowDx" /> <attr name="layout_shadowDy" /> </declare-styleable>
注意这里,为了在所以子View中使用自定义属性,我们需要将所有的属性定义成layout开头
使用效果如下:最外层使用HConstraintLayout
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。