这篇文章给大家介绍了利用 Android 自定义 View 实现水面上涨效果,对大家日常开发很有帮助,有需要的朋友们可以参考借鉴。
Android 是一种基于 Linux 的自由及开放源代码的操作系统,主要使用于移动设备,如智能手机和平板电脑,由 Google 公司和开放手机联盟领导及开发。尚未有统一中文名称,中国大陆地区较多人使用 "安卓" 或 "安致"。
实现效果如下:
实现思路:
1、如何实现圆中水面上涨效果:利用 Paint 的 setXfermode 属性为 PorterDuff.Mode.SRC_IN 画出进度所在的矩形与圆的交集实现
2、如何水波纹效果:利用贝塞尔曲线,动态改变波峰值,实现 "随着进度的增加,水波纹逐渐变小的效果"
话不多说,看代码。
首先是自定义属性值,有哪些可自定义属性值呢?
圆的背景颜色:circle_color,进度的颜色:progress_color,进度显示文字的颜色:text_color,进度文字的大小:text_size,还有最后一个:波纹最大高度:ripple_topheight
- <declare-styleable name="WaterProgressView">
- <attr name="circle_color" format="color" />
- <!--圆的颜色-->
- <attr name="progress_color" format="color" />
- <!--进度的颜色-->
- <attr name="text_color" format="color" />
- <!--文字的颜色-->
- <attr name="text_size" format="dimension" />
- <!--文字大小-->
- <attr name="ripple_topheight" format="dimension" />
- <!--水页涟漪最大高度-->
- </declare-styleable>
下面是自定义 View:WaterProgressView 的部份代码:
成员变量
- public class WaterProgressView extends ProgressBar {
- //默认圆的背景色
- public static final int DEFAULT_CIRCLE_COLOR = 0xff00cccc;
- //默认进度的颜色
- public static final int DEFAULT_PROGRESS_COLOR = 0xff00CC66;
- //默认文字的颜色
- public static final int DEFAULT_TEXT_COLOR = 0xffffffff;
- //默认文字的大小
- public static final int DEFAULT_TEXT_SIZE = 18;
- //默认的波峰最高点
- public static final int DEFAULT_RIPPLE_TOPHEIGHT = 10;
- private Context mContext;
- private Canvas mPaintCanvas;
- private Bitmap mBitmap;
- //画圆的画笔
- private Paint mCirclePaint;
- //画圆的画笔的颜色
- private int mCircleColor;
- //画进度的画笔
- private Paint mProgressPaint;
- //画进度的画笔的颜色
- private int mProgressColor ;
- //画进度的path
- private Path mProgressPath;
- //贝塞尔曲线波峰最大值
- private int mRippleTop = 10;
- //进度文字的画笔
- private Paint mTextPaint;
- //进度文字的颜色
- private int mTextColor;
- private int mTextSize = 18;
- //目标进度,也就是双击时处理任务的进度,会影响曲线的振幅
- private int mTargetProgress = 50;
- //监听双击和单击事件
- private GestureDetector mGestureDetector;
- }
获取自定义属性值:
- private void getAttrValue(AttributeSet attrs) {
- TypedArray ta = mContext.obtainStyledAttributes(attrs, R.styleable.WaterProgressView);
- mCircleColor = ta.getColor(R.styleable.WaterProgressView_circle_color,DEFAULT_CIRCLE_COLOR);
- mProgressColor = ta.getColor(R.styleable.WaterProgressView_progress_color,DEFAULT_PROGRESS_COLOR);
- mTextColor = ta.getColor(R.styleable.WaterProgressView_text_color,DEFAULT_TEXT_COLOR);
- mTextSize = (int) ta.getDimension(R.styleable.WaterProgressView_text_size, DesityUtils.sp2px(mContext,DEFAULT_TEXT_SIZE));
- mRippleTop = (int)ta.getDimension(R.styleable.WaterProgressView_ripple_topheight,DesityUtils.dp2px(mContext,DEFAULT_RIPPLE_TOPHEIGHT));
- ta.recycle();
- }
定义构造函数,注意
- mProgressPaint.setXfermode
- //当new该类时调用此构造函数
- public WaterProgressView(Context context) {
- this(context,null);
- }
- //当xml文件中定义该自定义View时调用此构造函数
- public WaterProgressView(Context context, AttributeSet attrs) {
- this(context, attrs,0);
- }
- public WaterProgressView(Context context, AttributeSet attrs, int defStyleAttr) {
- super(context, attrs, defStyleAttr);
- this.mContext = context;
- getAttrValue(attrs);
- //初始化画笔的相关属性
- initPaint();
- mProgressPath = new Path();
- }
- private void initPaint() {
- //初始化画圆的paint
- mCirclePaint = new Paint();
- mCirclePaint.setColor(mCircleColor);
- mCirclePaint.setStyle(Paint.Style.FILL);
- mCirclePaint.setAntiAlias(true);
- mCirclePaint.setDither(true);
- //初始化画进度的paint
- mProgressPaint = new Paint();
- mProgressPaint.setColor(mProgressColor);
- mProgressPaint.setAntiAlias(true);
- mProgressPaint.setDither(true);
- mProgressPaint.setStyle(Paint.Style.FILL);
- //其实mProgressPaint画的也是矩形,当设置xfermode为PorterDuff.Mode.SRC_IN后则显示的为圆与进度矩形的交集,则为半圆
- mProgressPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
- //初始化画进度文字的画笔
- mTextPaint = new Paint();
- mTextPaint.setColor(mTextColor);
- mTextPaint.setStyle(Paint.Style.FILL);
- mTextPaint.setAntiAlias(true);
- mTextPaint.setDither(true);
- mTextPaint.setTextSize(mTextSize);
- }
方法代码:
- onMeasure()
- @Override
- protected synchronized void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- //使用时,需要明确定义该View的尺寸,即用测量模式为MeasureSpec.EXACTLY
- int width = MeasureSpec.getSize(widthMeasureSpec);
- int height = MeasureSpec.getSize(heightMeasureSpec);
- setMeasuredDimension(width,height);
- //初始化Bitmap,让所有的drawCircle,drawPath,drawText都draw在该bitmap所在的canvas上,然后再将该bitmap 画在onDraw方法的canvas上,
- //所以此bitmap的width,height需要减去left,top,right,bottom的padding
- mBitmap = Bitmap.createBitmap(width-getPaddingLeft()-getPaddingRight(),height- getPaddingTop()-getPaddingBottom(), Bitmap.Config.ARGB_8888);
- mPaintCanvas = new Canvas(mBitmap);
- }
接下来是核心部份,onDraw 中的代码。我们先将 Circle,进度条,进度文字 draw 到自定义 canvas 的 bitmap 上,再将此 bitmap draw 到 onDraw 方法中的 canvas 上。drawCircle 与 drawText 应该没什么难度,关键点就在于画进度条,怎么画呢?既然有水波纹效果,有曲线,就用 drawPath 了。
drawPath 的流程如下:
其中 ratio 的代码如下,即 ratio 为当前进度占总进度的百分比
- float ratio = getProgress()*1.0f/getMax();
因为坐标是从 B 点向下和向右正向延伸的,则 A 点的坐标为(width,(1-ratio)*height), 其中 width 为 bitmap 的宽,height 为 bitmap 的高。我们先将 mProgressPath.moveTo 到 A 点,然后从 A 点顺时针方向确定 path 的各个关键点,如图,则代码如下:
- int rightTop = (int) ((1-ratio)*height);
- mProgressPath.moveTo(width,rightTop);
- mProgressPath.lineTo(width,height);
- mProgressPath.lineTo(0,height);
- mProgressPath.lineTo(0,rightTop);
如此 mProgressPath 已经 lineTo 到了 C 点,需要在 A 点与 C 点之间形成水波纹效果,则需要在 A 点与 C 点间画贝塞尔曲线。
我们设定波峰最高点为 10,则一段波长为 40,需要画
段这样的曲线,则画曲线的代码如下:
- width*1.0f/40
- int count = (int) Math.ceil(width*1.0f/(10 *4));
- for(int i=0; i<count; i++) {
- mProgressPath.rQuadTo(10,10,2* 10,0);
- mProgressPath.rQuadTo(10,-10,2* 10,0);
- }
- mProgressPath.close();
- mPaintCanvas.drawPath(mProgressPath,mProgressPaint);
这样就能画出水面上涨且有波纹效果的进度条了。但我们还要实现随着水面上涨,越接近目标进度,水面波纹应该越来越小,则应该把 10 抽出为变量定义为 mRippleTop 等初始时波峰最大值,然后定义 top 为随着进度不断接近目标进度时曲线的实时波峰值 ,其中 mTargetProgress 为目标 progress,因为有一个目标进度才能实现当前进度不断接近目标进度的过程中,水面渐趋于平面的效果:
- float top = (mTargetProgress-getProgress())*1.0f/mTargetProgress* mRippleTop;
所以 drawPath 的代码更新如下:
- float top = (mTargetProgress-getProgress())*1.0f/mTargetProgress* mRippleTop;
- for(int i=0; i<count; i++) {
- mProgressPath.rQuadTo(mRippleTop,top,2* mRippleTop,0);
- mProgressPath.rQuadTo(mRippleTop,-top,2* mRippleTop,0);
- }
如此就能真正实现水面上涨的进度条了。
但如何实现图中双击时水面从 0% 上涨到目标进度,单击时水面在目标进度不断涌动的效果呢?
先说说双击效果的实现:这个简单,定义一个 Handler,,当双击时,
,每隔一段时间
- handler.postDelayed(runnable,time)
,在 runnable 中
- progress+1
不断更新进度,直到当前 progress 到达 mTargetProgress。
- invalidate()
代码如下
- /**
- * 实现双击动画
- */
- private void startDoubleTapAnimation() {
- setProgress(0);
- doubleTapHandler.postDelayed(doubleTapRunnable,60);
- }
- private Handler doubleTapHandler = new Handler(){
- @Override
- public void handleMessage(Message msg) {
- super.handleMessage(msg);
- }
- };
- //双击处理线程,隔60ms发送一次数据
- private Runnable doubleTapRunnable = new Runnable() {
- @Override
- public void run() {
- if(getProgress() < mTargetProgress) {
- invalidate();
- setProgress(getProgress()+1);
- doubleTapHandler.postDelayed(doubleTapRunnable,60);
- } else {
- doubleTapHandler.removeCallbacks(doubleTapRunnable);
- }
- }
- };
双击效果实现了,那如何实现单击效果呢?单击时要求水面不断涌动一段时间,水面波纹逐渐变小,然后水面变平。我们可以定义一个 mSingleTapAnimationCount 变量为水面涌动的次数,然后像双击时的处理一样,定义一个 Handler 隔一段时间发送一次更新界面的 message,
,然后我们交替地让初始时的波峰一次为正一次为负,则能实现水面涌动的效果。
- mSingleTapAnimationCount--
核心代码如下:
- private void startSingleTapAnimation() {
- isSingleTapAnimation = true;
- singleTapHandler.postDelayed(singleTapRunnable,200);
- }
- private Handler singleTapHandler = new Handler(){
- @Override
- public void handleMessage(Message msg) {
- super.handleMessage(msg);
- }
- };
- //单击处理线程,隔200ms发送一次数据
- private Runnable singleTapRunnable = new Runnable() {
- @Override
- public void run() {
- if(mSingleTapAnimationCount > 0) {
- invalidate();
- mSingleTapAnimationCount--;
- singleTapHandler.postDelayed(singleTapRunnable,200);
- } else {
- singleTapHandler.removeCallbacks(singleTapRunnable);
- //是否正在进行单击动画
- isSingleTapAnimation = false;
- //重置单击动画运行次数为50次
- mSingleTapAnimationCount = 50;
- }
- }
- };
onDraw 中的代码作相应的更改,因单击与双击时 drawPath 中曲线部分的绘制逻辑不一样,则我们定义一个变量 isSingleTapAnimation 区别是正在进行单击动画还是在进行双击动画。
更改后的代码如下:
- //画进度
- mProgressPath.reset();
- //从右上边开始draw path
- int rightTop = (int) ((1-ratio)*height);
- mProgressPath.moveTo(width,rightTop);
- mProgressPath.lineTo(width,height);
- mProgressPath.lineTo(0,height);
- mProgressPath.lineTo(0,rightTop);
- //画贝塞尔曲线,形成波浪线
- int count = (int) Math.ceil(width*1.0f/(mRippleTop *4));
- //不是单击animation状态
- if(!isSingleTapAnimation&&getProgress()>0) {
- float top = (mTargetProgress-getProgress())*1.0f/mTargetProgress* mRippleTop;
- for(int i=0; i<count; i++) {
- mProgressPath.rQuadTo(mRippleTop,-top,2* mRippleTop,0);
- mProgressPath.rQuadTo(mRippleTop,top,2* mRippleTop,0);
- }
- } else {
- //单击animation状态,为了将效果放大,将mRippleTop放大2倍
- //同时偶数时曲线走向如图所示,奇数时则曲线刚好相反
- float top = (mSingleTapAnimationCount*1.0f/50)*10;
- //奇偶数时曲线切换
- if(mSingleTapAnimationCount%2==0) {
- for(int i=0; i<count; i++) {
- mProgressPath.rQuadTo(mRippleTop *2,top*2,2* mRippleTop,0);
- mProgressPath.rQuadTo(mRippleTop *2,-top*2,2* mRippleTop,0);
- }
- } else {
- for(int i=0; i<count; i++) {
- mProgressPath.rQuadTo(mRippleTop *2,-top*2,2* mRippleTop,0);
- mProgressPath.rQuadTo(mRippleTop *2,top*2,2* mRippleTop,0);
- }
- }
- }
- mProgressPath.close();
- mPaintCanvas.drawPath(mProgressPath,mProgressPaint);
基本上重要的代码与核心逻辑与代码就在上面了。
注意点:
1、当 drawCircle 时要考虑到 padding,则 circle 的宽和高为 getWidth 与 getHeight 减去 padding 值,代码如下:
- //自定义bitmap的宽和高
- int width = getWidth()-getPaddingLeft()-getPaddingRight();
- int height = getHeight()-getPaddingTop()-getPaddingBottom();
- //画圆
- mPaintCanvas.drawCircle(width/2,height/2,height/2,mCirclePaint);
2、当 drawText 时,不是从 text 的 height 的中间开始 draw 的,而是从 baseline 开始 draw 的
那如何获取 baseline 的 height 坐标呢
- Paint.FontMetrics metrics = mTextPaint.getFontMetrics();
- //因为ascent在baseline之上,所以ascent为负数。descent+ascent为负数,所以是减而不是加
- float baseLine = height*1.0f/2 - (metrics.descent+metrics.ascent)/2;
drawText 的全部代码如下:
- //画进度文字
- String text = ((int)(ratio*100))+"%";
- //获得文字的宽度
- float textWidth = mTextPaint.measureText(text);
- Paint.FontMetrics metrics = mTextPaint.getFontMetrics();
- //descent+ascent为负数,所以是减而不是加
- float baseLine = height*1.0f/2 - (metrics.descent+metrics.ascent)/2;
- mPaintCanvas.drawText(text,width/2-textWidth/2,baseLine,mTextPaint);
3、因为要顾及到 padding,记得将 onDraw 中的 canvas translate 到
处。
- (getPaddingLeft(),getPaddingTop())
- canvas.translate(getPaddingLeft(),getPaddingTop());
- canvas.drawBitmap(mBitmap,0,0,null);
最后记得将自定义的 bitmap draw 到 onDraw 中的 canvas 上。到这儿自定义水面上涨效果的进度条于写完了。
总结
以上就是这篇文章的全部内容,希望本文的内容对大家的学习或者工作带来一定的帮助,如果有疑问大家可以留言交流。
来源: http://www.phperz.com/article/17/0320/295677.html