最近在项目中写了一个自定义的倒计时控件,效果是倒计时开始后,红心逐渐被填充满。效果如下图:
分为两部分:计时器和绘制Bitmap。
计时器使用Timer和TimerTask,每个一秒执行一次TimerTask的run函数,使控件重绘。代码如下:
mTimer = new Timer();
mTimerTask = new TimerTask() {
@Override
public void run() {
postInvalidate();
synchronized (this) {
if (index > 59) {
index = 1;
mTimer.cancel();
}
index++;
}
}
};
mTimer.schedule(mTimerTask, 1000, 1000);
绘制的思路大概是:
1.从图片资源中解析得到Bitmap,获得其Width和Height;
2.重载onMeasure和onSizeChanged函数,设置并得到控件的宽和高;
3.使用PorterDuffXfermode图形混合模式来得到所需的Bitmap;
4.重载onDraw函数,在函数中,将上一步所得到的Bitmap缩放至控件大小以显示出来。
下面我们来看一下几个重要部分,其余代码最后会附上。
1.重载onMeasure函数完成控件大小的测量
@Override
public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int resultW = 0;
if (MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY) {
resultW = MeasureSpec.getSize(widthMeasureSpec);
} else {
if (MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.AT_MOST) {
resultW = Math.min(bWidth,
MeasureSpec.getSize(widthMeasureSpec));
}
}
int resultH = 0;
if (MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY) {
resultH = MeasureSpec.getSize(heightMeasureSpec);
} else {
if (MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.AT_MOST) {
resultH = Math.min(bHeight,
MeasureSpec.getSize(heightMeasureSpec));
}
}
setMeasuredDimension(resultW, resultH);
}
如果我在xml文件中设置如下:
<com.example.grownheart.GrownHeart
android:id="@+id/grownHeart"
android:layout_width="100dp"
android:layout_height="100dp"
/>
我们对控件的宽和高设置的是具体的值:100dp,那么onMeasure函数在测量控件的宽高时所得的 widthMeasureSpec/heightMeasureSpec的getMode就是
MeasureSpec.EXACTLY,则控件的宽高就是getSize,就是我们设置的100dp。
如果我们在xml中配置如下:
<com.example.grownheart.GrownHeart
android:id="@+id/grownHeart2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
那么widthMeasureSpec/heightMeasureSpec的getMode就是MeasureSpec.AT_MOST,这时候控件的宽高就是图片资源宽(或高)与父容器中剩余宽(或高)两者中比较小
的那个。
2.在onSizeChanged函数中获得控件的宽和高
@Override
public void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
width = w;
height = h;
bm = Bitmap.createBitmap(bWidth, bHeight, Bitmap.Config.ARGB_8888);
mCanvas = new Canvas(bm);
}
参数中的int w和int h,分别是控件的宽,高。
3.在onDraw中通过图形的混合得到期望的Bitmap。
@Override
public void onDraw(Canvas canvas) {
canvas.drawBitmap(
Bitmap.createScaledBitmap(makeBitmap(), width, height, true),
0, 0, mPaint);
}
// 绘制Bitmap
public Bitmap makeBitmap() {
// 先绘制底层图片
mCanvas.drawBitmap(charm, 0, 0, mPaint);
int i = mCanvas.saveLayer(0, 0, bm.getWidth(), bm.getHeight(), null,
Canvas.ALL_SAVE_FLAG);
mPaint.setColor(Color.RED);
Log.i("GrownHeart", "onDraw:index=" + index);
mCanvas.drawRect(
new RectF(0f, (60 - index) * H, bm.getWidth(), bm.getHeight()),
mPaint);
mPaint.setXfermode(modeIn);
mCanvas.drawBitmap(charm_on, 0, 0, mPaint);
mPaint.setXfermode(null);
mCanvas.restoreToCount(i);
return bm;
}
边框图和实心图如下:
PorterDuffXfermode(PorterDuff.Mode.DST_IN));其次将实心图绘制到图层中,这样就能得到重叠区域。然后将图层上所绘制的restore。最后通过createScaledBitmap将
Bitmap缩放至控件大小并显示。
源代码
public class GrownHeart extends View {
public Timer mTimer;
public TimerTask mTimerTask;
public int bWidth;// Bitmap宽度
public int bHeight;// Bitmap高度
public int width;// 控件宽度
public int height;// 控件高度
public Bitmap charm;// 资源位图
public Bitmap charm_on;// 资源位图
public Bitmap bm;
public Canvas mCanvas;
public Paint mPaint;
public float H;
private static int index;
public static final PorterDuffXfermode modeIn;
public static final PorterDuffXfermode modeOut;
static {
modeIn = new PorterDuffXfermode(PorterDuff.Mode.DST_IN);
modeOut = new PorterDuffXfermode(PorterDuff.Mode.SRC_IN);
}
public GrownHeart(Context context) {
super(context);
init();
}
public GrownHeart(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public void init() {
mTimer = new Timer();
mTimerTask = new TimerTask() {
@Override
public void run() {
postInvalidate();
synchronized (this) {
if (index > 59) {
index = 1;
mTimer.cancel();
}
Log.i("GrownHeart", "TimerTask1:index=" + index);
index++;
Log.i("GrownHeart", "TimerTask2:index=" + index);
}
}
};
charm = BitmapFactory.decodeResource(getResources(),
R.drawable.chatroom_charm).copy(Bitmap.Config.ARGB_8888, true);
charm_on = BitmapFactory.decodeResource(getResources(),
R.drawable.chatroom_charm_on).copy(Bitmap.Config.ARGB_8888,
true);
bWidth = charm_on.getWidth();
bHeight = charm_on.getHeight();
H = bHeight / 60F;
index = 1;
mPaint = new Paint();
mPaint.setAntiAlias(true);
mPaint.setFilterBitmap(false);
}
public void startTimer() {
mTimer.schedule(mTimerTask, 1000, 1000);
}
@Override
public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int resultW = 0;
if (MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY) {
resultW = MeasureSpec.getSize(widthMeasureSpec);
} else {
if (MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.AT_MOST) {
resultW = Math.min(bWidth,
MeasureSpec.getSize(widthMeasureSpec));
}
}
int resultH = 0;
if (MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY) {
resultH = MeasureSpec.getSize(heightMeasureSpec);
} else {
if (MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.AT_MOST) {
resultH = Math.min(bHeight,
MeasureSpec.getSize(heightMeasureSpec));
}
}
setMeasuredDimension(resultW, resultH);
}
@Override
public void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
width = w;
height = h;
bm = Bitmap.createBitmap(bWidth, bHeight, Bitmap.Config.ARGB_8888);
mCanvas = new Canvas(bm);
}
// 绘制Bitmap
public Bitmap makeBitmap() {
// 先绘制底层图片
mCanvas.drawBitmap(charm, 0, 0, mPaint);
int i = mCanvas.saveLayer(0, 0, bm.getWidth(), bm.getHeight(), null,
Canvas.ALL_SAVE_FLAG);
mPaint.setColor(Color.RED);
Log.i("GrownHeart", "onDraw:index=" + index);
mCanvas.drawRect(
new RectF(0f, (60 - index) * H, bm.getWidth(), bm.getHeight()),
mPaint);
mPaint.setXfermode(modeIn);
mCanvas.drawBitmap(charm_on, 0, 0, mPaint);
mPaint.setXfermode(null);
mCanvas.restoreToCount(i);
return bm;
}
@Override
public void onDraw(Canvas canvas) {
canvas.drawBitmap(
Bitmap.createScaledBitmap(makeBitmap(), width, height, true),
0, 0, mPaint);
}
}
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
tools:context="com.example.grownheart.MainActivity" >
<com.example.grownheart.GrownHeart
android:id="@+id/grownHeart"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</RelativeLayout>
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
GrownHeart grownHeart=(GrownHeart)findViewById(R.id.grownHeart);
grownHeart.startTimer();
}
}