一、前言
PorterDuffXfermode 作为Android重要的合成组件,可以通过区域叠加方式进行裁剪和合成,起作用和Path.Op 类似,对于音视频开发中使用蒙版抠图和裁剪的需求,这种情况一般生成不了Path时,因此只能使用PorterDuffXfermode进行合成,当然Paint设置Shader也具备一定的能力,但是还是无法做到很多效果。
二、案例
这个案例使用了Bitmap合成,在边缘区域对色彩裁剪,从而实现了圆觉裁剪。
模版
//裁剪区域
技术上没有太多难点,但要注意的是Xfermode是2个Bitmap之间只使用,不像Shader那样可以单独使用。
ini
Canvas resultCanvas = new Canvas(resultBitmap);
resultCanvas.drawBitmap(dstBitmap, paddingLeft + point.x, paddingTop, mSolidPaint);
mSolidPaint.setXfermode(mPorterDuffXfermode);
resultCanvas.drawBitmap(srcRoundBitmap, 0, 0, mSolidPaint);
canvas.drawBitmap(resultBitmap, 0, 0, null);
另外一点就是速度计算,利用了没有时间的逼近减速公式,当然你可以使用动画去实现
arduino
float speed = Math.abs((mTargetZone - 1) * (contentWidth / mDivideNumber) - point.x) / mSpeed;
下面是速度控制逻辑
ini
@Override
public void run() {
//计算速度,先按照最大速度5变化,如果小于5,则表示该减速停靠
float speed = Math.abs((mTargetZone - 1) * (contentWidth / mDivideNumber) - point.x) / mSpeed;
speed = (float) Math.max(1f, speed);
float vPos = (mTargetZone - 1) * (contentWidth / mDivideNumber);
if (point.x < vPos) {
point.x += speed;
if (point.x > vPos) {
point.x = vPos;
}
} else {
point.x -= speed;
if (point.x < vPos) {
point.x = vPos;
}
}
if (point.x == vPos) {
isSliding = false;
} else {
isSliding = true;
postDelayed(this, 20);
}
postInvalidate();
}
全部逻辑
ini
public class TabBarView extends View implements Runnable {
//画笔
private Paint mSolidPaint;
//中间竖线与边框间隙
private int gapPadding = 0;
//平分量
private int mDivideNumber = 1;
//边框大小
private final float mBorderSize = 1.5f;
//避免重复绘制Bitmap,短暂保存底色bitmap
private Bitmap srcRoundBitmap;
//图片混合模式
private PorterDuffXfermode mPorterDuffXfermode;
private PointF point;
//内容区域大小
private float contentWidth;
private float contentHeight;
//滑动到的目标区域
private int mTargetZone;
//滑动速度
private float mSpeed;
//主调颜色
private int primaryColor;
//默认字体颜色
private int textColor;
//焦点字体颜色
private int selectedTextColor;
//item
private CharSequence[] mStringItems;
//字体大小
private float textSize;
//是否处于滑动
private boolean isSliding;
Bitmap dstBitmap;
Bitmap resultBitmap;
private RectF rectBound = new RectF();
public TabBarView(Context context) {
super(context);
init(null, 0);
}
public TabBarView(Context context, AttributeSet attrs) {
super(context, attrs);
init(attrs, 0);
}
public TabBarView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init(attrs, defStyle);
}
private void init(AttributeSet attrs, int defStyle) {
// Load attributes
final TypedArray a = getContext().obtainStyledAttributes(
attrs, R.styleable.TabBarView, defStyle, 0);
//参数值越大,速度越大,速度指数越小
mSpeed = Math.max(10 - Math.max(a.getInt(R.styleable.TabBarView_speed, 6), 6), 1);
mStringItems = a.getTextArray(R.styleable.TabBarView_tabEntries);
primaryColor = a.getColor(R.styleable.TabBarView_primaryColor, 0xFF4081);
textColor = a.getColor(R.styleable.TabBarView_textColor, primaryColor);
selectedTextColor = a.getColor(R.styleable.TabBarView_selectedTextColor, 0xffffff);
textSize = a.getDimensionPixelSize(R.styleable.TabBarView_textSize, 30);
if (mStringItems != null && mStringItems.length > 0) {
mDivideNumber = mStringItems.length;
}
a.recycle();
mSolidPaint = new Paint();
mSolidPaint.setFlags(Paint.ANTI_ALIAS_FLAG);
mPorterDuffXfermode = new PorterDuffXfermode(PorterDuff.Mode.SRC_IN);
point = new PointF(0, 0);
mTargetZone = 1;
invalidateTextPaintAndMeasurements();
}
private void invalidateTextPaintAndMeasurements() {
mSolidPaint.setColor(primaryColor);
mSolidPaint.setStrokeWidth(mBorderSize);
mSolidPaint.setTextSize(textSize);
mSolidPaint.setStyle(Paint.Style.STROKE);
mSolidPaint.setXfermode(null);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
recycleBitmap();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int paddingLeft = getPaddingLeft();
int paddingTop = getPaddingTop();
int paddingRight = getPaddingRight();
int paddingBottom = getPaddingBottom();
contentWidth = getWidth() - paddingLeft - paddingRight;
contentHeight = getHeight() - paddingTop - paddingBottom;
float minContentSize = Math.min(contentWidth, contentHeight);
rectBound.set(paddingLeft, paddingTop, paddingLeft + contentWidth, paddingTop + contentHeight);
canvas.drawRoundRect(rectBound, minContentSize / 2F, minContentSize / 2F, mSolidPaint);
for (int i = 1; i < mDivideNumber; i++) {
canvas.drawLine(paddingLeft + 1F * contentWidth * i / mDivideNumber, paddingTop + gapPadding, paddingLeft + contentWidth * i / mDivideNumber, paddingTop + contentHeight - gapPadding, mSolidPaint);
}
if (srcRoundBitmap == null) {
srcRoundBitmap = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_8888);
Canvas srcCanvas = new Canvas(srcRoundBitmap);
mSolidPaint.setStyle(Paint.Style.FILL_AND_STROKE);
srcCanvas.drawRoundRect(rectBound, minContentSize / 2F, minContentSize / 2F, mSolidPaint);
}
if(dstBitmap == null) {
dstBitmap = Bitmap.createBitmap((int) (contentWidth / mDivideNumber), (int) contentHeight, Bitmap.Config.ARGB_8888);
}
dstBitmap.eraseColor(Color.TRANSPARENT);
Canvas dstCanvas = new Canvas(dstBitmap);
dstCanvas.drawColor(Color.YELLOW);
if(resultBitmap == null) {
resultBitmap = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_8888);
}
resultBitmap.eraseColor(Color.TRANSPARENT);
Canvas resultCanvas = new Canvas(resultBitmap);
resultCanvas.drawBitmap(dstBitmap, paddingLeft + point.x, paddingTop, mSolidPaint);
mSolidPaint.setXfermode(mPorterDuffXfermode);
resultCanvas.drawBitmap(srcRoundBitmap, 0, 0, mSolidPaint);
canvas.drawBitmap(resultBitmap, 0, 0, null);
invalidateTextPaintAndMeasurements();
if (mStringItems != null) {
for (int i = 0; i < mStringItems.length; i++) {
String itemChar = mStringItems[i].toString();
float textX = (contentWidth / mDivideNumber) * i / 2 + paddingLeft + (contentWidth * (i + 1) / mDivideNumber - mSolidPaint.measureText(itemChar)) / 2;
float textY = paddingTop + (contentHeight - mSolidPaint.getFontMetrics().bottom - mSolidPaint.getFontMetrics().ascent) / 2;
int color = mSolidPaint.getColor();
mSolidPaint.setStyle(Paint.Style.FILL);
if ((i + 1) == mTargetZone && !isSliding) {
mSolidPaint.setColor(selectedTextColor);
} else {
mSolidPaint.setColor(textColor);
}
canvas.drawText(itemChar, textX, textY, mSolidPaint);
mSolidPaint.setColor(color);
mSolidPaint.setStyle(Paint.Style.STROKE);
}
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
if (checkLocationIsOk(event) && !isSliding) {
return true;
}
break;
case MotionEvent.ACTION_MOVE:
return checkLocationIsOk(event);
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_OUTSIDE:
if (checkLocationIsOk(event) && !isSliding) {
float x = event.getX() - getPaddingLeft();
mTargetZone = (int) (x / (contentWidth / mDivideNumber)) + 1;
//规避区域超出范围
mTargetZone = Math.min(mTargetZone, mDivideNumber);
postToMove();
}
break;
}
return super.onTouchEvent(event);
}
private void postToMove() {
if (point.x == (mTargetZone - 1) * (contentWidth / mDivideNumber)) {
return;
}
postDelayed(this, 20);
}
/**
* 检测位置是否可用
*
* @param event
* @return
*/
private boolean checkLocationIsOk(MotionEvent event) {
float x = event.getX();
float y = event.getY();
if (x - getPaddingLeft() > 0 && (getPaddingLeft() + contentWidth - x) > 0 && y - getPaddingTop() > 0 && (getPaddingTop() + contentHeight - y) > 0) {
return true;
}
return false;
}
private void recycleBitmap(Bitmap bmp) {
if (bmp != null && !bmp.isRecycled()) {
bmp.recycle();
}
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
getHandler().removeCallbacksAndMessages(null);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
if (widthMode != MeasureSpec.EXACTLY) {
widthSize = getResources().getDisplayMetrics().widthPixels / 2;
}
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
if (heightMode != MeasureSpec.EXACTLY) {
heightSize = widthSize / 2;
}
setMeasuredDimension(widthSize, heightSize);
}
@Override
public void run() {
//计算速度,先按照最大速度5变化,如果小于5,则表示该减速停靠
float speed = Math.abs((mTargetZone - 1) * (contentWidth / mDivideNumber) - point.x) / mSpeed;
speed = (float) Math.max(1f, speed);
float vPos = (mTargetZone - 1) * (contentWidth / mDivideNumber);
if (point.x < vPos) {
point.x += speed;
if (point.x > vPos) {
point.x = vPos;
}
} else {
point.x -= speed;
if (point.x < vPos) {
point.x = vPos;
}
}
if (point.x == vPos) {
isSliding = false;
} else {
isSliding = true;
postDelayed(this, 20);
}
postInvalidate();
}
public void setSelectedTab(int tabIndex) {
mTargetZone = Math.max(Math.min(mDivideNumber, tabIndex + 1), 1);
recycleBitmap();
postToMove();
}
public void setTabItems(CharSequence[] mStringItems) {
this.mStringItems = mStringItems;
recycleBitmap();
invalidate();
}
private void recycleBitmap() {
if(dstBitmap != null && !dstBitmap.isRecycled()){
dstBitmap.recycle();
}
if(resultBitmap != null && !resultBitmap.isRecycled()){
resultBitmap.recycle();
}
resultBitmap = null;
dstBitmap = null;
}
}
我们需要自定义一些属性
ini
<declare-styleable name="TabBarView">
<attr name="speed" format="integer" />
<attr name="tabEntries" format="reference"/>
<attr name="primaryColor" format="color|reference"/>
<attr name="textSize" format="dimension"/>
<attr name="textColor" format="color|reference"/>
<attr name="selectedTextColor" format="color|reference"/>
</declare-styleable>
还有部分需要引用的 string-array
typescript
<string-array name="tabEntries_array">
<item>A</item>
<item>B</item>
<item>C</item>
<item>D</item>
</string-array>
然后是布局文件(片段)
ini
<com.android.jym.widgets.TabBarView
android:layout_width="match_parent"
android:layout_height="40dp"
android:background="@android:color/transparent"
android:padding="10dp"
app:speed="4"
app:tabEntries="@array/tabEntries_array"
app:primaryColor="@color/colorAccent"
app:textColor="@color/colorPrimaryDark"
app:selectedTextColor="@android:color/white"
/>
三、总结
使用Xfermode + 蒙版进行抠图,是Android中重要的工具,本篇作为技术储备,后续会通过这种方式实现一些新的功能。