最近在项目中写了一个自定义的倒计时控件,效果是倒计时开始后,红心逐渐被填充满。效果如下图:
分为两部分:计时器和绘制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; }
边框图和实心图如下:
首先先将边框图绘制到Bitmap中,然后新建Canvas图层,在该图层上绘制红色矩形,该矩形的高度每次是改变的。然后设置Piant的图形混合模式,mPaint.setXfermode(new
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(); } }