Android 自定义View之BounceProgressBar
之前几天下载了很久没用了的桌面版酷狗来用用的时候,发现其中加载歌曲的等待进度条的效果不错(个人感觉),如下:
然后趁着这周末两天天气较冷,窝在宿舍放下成堆的操作系统作业(目测要抄一节多课的一堆堆文字了啊...啊..)毅然决定把它鼓捣出来,最终的效果如下(总感觉有点不和谐啊·):
对比能看出来的就是多了形状的选择还有使用图片了,那么接下来就是它的实现过程。
对自定义View实现还不明白的建议看下郭神的博客(View系列4篇): Android LayoutInflater原理分析,带你一步步深入了解View(一) 和大苞米的这篇:ANDROID自定义视图——onMeasure,MeasureSpec源码 流程 思路详解
自定义属性
自定义View一般都要用到view本身的属性了,重写现有的控件则不用。额,然后我们的这个BounceProgressBar需要什么特有的属性呢?首先要明确的是这里BounceProgressBar没有提供具体进度表现的实现的。再具体想想:它需要每个图像的大小,叫singleSrcSize,类型就是dimension了;上下跳动的速度,叫speed,类型为integer;形状,叫shape,类型为枚举类型,提供这几个形状的实现,original、circle、pentagon、rhombus、heart都是见名知意的了;最后是需要的图片资源,叫src,类型为reference|color,即可以是drawable里的图片或颜色值。
有了需要的属性后,在values文件夹下建个资源文件(名字随意,见名知意就好)来定义这些属性了,如下,代码可能有些英文,而且水平有些渣,不过一般前面都会解释了的:
<?xml version="1.0" encoding="utf-8"?>
<resources> <declare-styleable name="BounceProgressBar"> <!-- the single child size -->
<attr name="singleSrcSize" format="dimension" />
<!-- the bounce animation one-way duration -->
<attr name="speed" format="integer" />
<!-- the child count ,本来还想能自定义个数的,但是暂时个人实现起来有些麻烦,所以先不加这个-->
<!-- <attr name="count" format="integer" min="" /> -->
<!-- the progress child shape -->
<attr name="shape" format="enum">
<enum name="original" value="" />
<enum name="circle" value="" />
<enum name="pentagon" value="" />
<enum name="rhombus" value="" />
<enum name="heart" value="" />
</attr>
<!-- the progress drawable resource -->
<attr name="src" format="reference|color"></attr>
</declare-styleable> </resources>
然后先把BounceProgressBar类写出来如下:
public class BounceProgressBar extends View {
//...
}
现在就可以在布局里用我们的BounceProgressBar了,这里需要注意的是,我们需要加上下面代码第二行命名空间才能使用我们的属性,也可以把它放到根元素的属性里。
<org.roc.bounceprogressbar.BounceProgressBar
xmlns:bpb="http://schemas.android.com/apk/res-auto"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_centerVertical="true"
bpb:shape="circle"
bpb:singleSrcSize="8dp"
bpb:speed=""
bpb:src="#6495ED" />
自定义了属性最后我们要做的就是在代码里去获取它了,在哪里获取呢,当然是BounceProgressBar类的构造方法里了,相关代码如下:
public BounceProgressBar(Context context) {
this(context, null, );
} public BounceProgressBar(Context context, AttributeSet attrs) {
this(context, attrs, );
} public BounceProgressBar(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(attrs);
} private void init(AttributeSet attrs) {
if (null == attrs) {
return;
}
TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.BounceProgressBar);
speed = a.getInt(R.styleable.BounceProgressBar_speed, );
size = a.getDimensionPixelSize(R.styleable.BounceProgressBar_singleSrcSize, );
shape = a.getInt(R.styleable.BounceProgressBar_shape, );
src = a.getDrawable(R.styleable.BounceProgressBar_src);
a.recycle();
}
得到属性还是比较简单的,记得把TypedArray回收掉。首先是获得我们定义的TypedArray,然后是一个一个的去get属性值。然后可能有人要说了,我明明没定义R.styleable.BounceProgressBar_xxx这些东西啊,其实呢这是Android自动给我们生成的declare-styleable里的每个属性的在TypedArray里的index对应位置的,你是找不到类似R.styleable.speed这种东西存在的,它又是怎么对应的呢,点进去看一下R文件就知道了,R.styleable.BounceProgressBar_speed的值是1,因为speed是第2个属性(0,1..),所以你确定属性的位置直接写a.getInt(1, 250)也是可以的。第二个参数是默认值。
图形的形状
得到属性值后,我们就可以去做相应的处理操作了,这里是图形形状的获取,用到了shape、src和size属性,speed和size在下一点中也会讲到。
首先我们观察到三个图片是有些渐变的效果的,我这里只是简单地做透明度处理,即一次变透明,效果是可以在处理好一点,可能之后再优化了。从src得到的图片资源是Drawable的,无论是ColorDrawable或是BitmapDrawable。我们需要先把它转换成size大小的Bitmap,再用canvas对它进行形状裁剪操作。至于为什么要先转Bitmap呢,这是我的做法,再看完下面的操作后如果有更好的方式希望可以交流一下。
/**
* Drawable → Bitmap(the size is "size")
*/
private Bitmap drawable2Bitmap(Drawable drawable) {
Bitmap bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
drawable.setBounds(, , size, size);
drawable.draw(canvas);
return bitmap;
}
Bitmap得到了,形状呢我们就可以进行操作了,我们先说圆形circle、菱形rhombus、五角星pentagon,再说心形heart,因为处理方式有些不同。像其它ShapeImageView我看到好像喜欢用svg来处理,看了他们的代码,例如这个:https://github.com/siyamed/android-shape-imageview 貌似有些麻烦,相比之下我的处理比较简单。
圆形circle、菱形rhombus、五角星pentagon
这些形状都可以使用ShapeDrawable来得到。我们需要BitmapShader渲染器,这是ShapeDrawable的Paint画笔需要的,再需要一个空的位图Bitmap,再一个 Canvas。如下:
BitmapShader bitmapShader = new BitmapShader(srcBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
Bitmap bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
Path path;
ShapeDrawable shapeDrawable = new ShapeDrawable();
shapeDrawable.getPaint().setAntiAlias(true);
shapeDrawable.getPaint().setShader(bitmapShader);
shapeDrawable.setBounds(, , size, size);
shapeDrawable.setAlpha(alpha);
Canvas是ShapeDrawable上的画布,BitmapShader是ShapeDrawable画笔Paint的的渲染器,用来渲染处理图形(由src的drawable转换得到的bitmap),渲染模式选用了CLAMP,意思是 如果渲染器超出原始边界范围,会复制范围内边缘染色。
圆形呢,我们直接用现成的就可以:
shapeDrawable.setShape(new OvalShape());
这个ShapeDrawable画出来的就是圆形了,当然要调用shapeDrawable.draw(canvas);方法了,这样bitmap就会变成圆形的srcBitmap(方法传进的参数)了,这方法的完整代码后面给出。
菱形呢,我们则这样子:
path = new Path();
path.moveTo(size / , );
path.lineTo(, size / );
path.lineTo(size / , size);
path.lineTo(size, size / );
path.close();
shapeDrawable.setShape(new PathShape(path, size, size));
就是边长为size的正方形,取每条边的中点,四个点连起来就是了。我们知道Android的坐标一般都是屏幕左上角顶点为坐标原点的,坐标点找到了我们把path连接起来即close。这样PathShape就是一个菱形了。多边形差不多都可以这么画的,下面的五角形也是一样。说明:这里所有图形的绘制都是在边长size的正方形里。
五角形的原理也是用PathShape,只是它需要的坐标点有点多啊,需要仔细计算慢慢调试。
path = new Path();
// The Angle of the pentagram
float radian = (float) (Math.PI * / );
float radius = size / ;
// In the middle of the radius of the pentagon
float radius_in = (float) (radius * Math.sin(radian / ) / Math.cos(radian));
// The starting point of the polygon
path.moveTo((float) (radius * Math.cos(radian / )), );
path.lineTo((float) (radius * Math.cos(radian / ) + radius_in * Math.sin(radian)),
(float) (radius - radius * Math.sin(radian / )));
path.lineTo((float) (radius * Math.cos(radian / ) * ),
(float) (radius - radius * Math.sin(radian / )));
path.lineTo((float) (radius * Math.cos(radian / ) + radius_in * Math.cos(radian / )),
(float) (radius + radius_in * Math.sin(radian / )));
path.lineTo((float) (radius * Math.cos(radian / ) + radius * Math.sin(radian)),
(float) (radius + radius * Math.cos(radian)));
path.lineTo((float) (radius * Math.cos(radian / )), (float) (radius + radius_in));
path.lineTo((float) (radius * Math.cos(radian / ) - radius * Math.sin(radian)),
(float) (radius + radius * Math.cos(radian)));
path.lineTo((float) (radius * Math.cos(radian / ) - radius_in * Math.cos(radian / )),
(float) (radius + radius_in * Math.sin(radian / )));
path.lineTo(, (float) (radius - radius * Math.sin(radian / )));
path.lineTo((float) (radius * Math.cos(radian / ) - radius_in * Math.sin(radian)),
(float) (radius - radius * Math.sin(radian / )));
path.close();// Make these points closed polygons
shapeDrawable.setShape(new PathShape(path, size, size));
连线果然有点多啊。。这里的绘制五角形是先根据指定的五角形的角的角度还有半径,然后确定连线起点,再连下一点...最后封闭,一不小心就不知道连到哪去了。。
心形heart
path来画心形就不能连直线实现了,刚开始是使用path的quadTo(x1, y1, x2, y2)方法来画贝塞尔曲线来实现的,发现画出来的形状不饱满,更像一个锥形(脑补),所以就放弃这种方式了。然后找到了这篇关于画心形的介绍Heart Curve,然后就采用他的第四种方法(如下图),即采用两个椭圆形状来裁剪实现。
1、画一个椭圆形状
//canvas bitmap bitmapshader等,上面代码已有
path = new Path();
Paint paint = new Paint();
paint.setAntiAlias(true);
paint.setShader(bitmapShader);
Matrix matrix = new Matrix(); //控制旋转
Region region = new Region();//裁剪一段图形区域
RectF ovalRect = new RectF(size / , , size - (size / ), size);
path.addOval(ovalRect, Path.Direction.CW);
2、旋转图形,大概45度左右
matrix.postRotate(, size / , size / );
path.transform(matrix, path);
3、选取旋转后的右半部分图形,并用cancas画出这半边的心形
path.transform(matrix, path);
region.setPath(path, new Region((int) size / , , (int) size, (int) size));
canvas.drawPath(region.getBoundaryPath(), paint);
4、重复1、2、3同时改变方向角度和裁剪的区域
matrix.reset();
path.reset();
path.addOval(ovalRect, Path.Direction.CW);
matrix.postRotate(-, size / , size / );
path.transform(matrix, path);
region.setPath(path, new Region(, , (int) size / , (int) size));
canvas.drawPath(region.getBoundaryPath(), paint);
这样我们便完成心形图片的裁剪工作了,得到的bitmap就变成心形了:
这个心可以见人了。。
画完心就该下一步了。
View的绘制
说到view的绘制过程就需要下面三部曲了:
- 测量——onMeasure():决定View的大小
- 布局——onLayout():决定View在ViewGroup中的位置
- 绘制——onDraw():如何绘制这个View。
测量
对于BounceProgressBar控件的测量还是比较简单的,当wrap_content时高度和宽度分别为size的5倍和4倍,其它情况时就指定宽高为具体测量到的值就好。然后决定三个图形在控件之中的水平位置:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec); int sizeWidth = MeasureSpec.getSize(widthMeasureSpec);
int sizeHeight = MeasureSpec.getSize(heightMeasureSpec);
int modeWidth = MeasureSpec.getMode(widthMeasureSpec);
int modeHeight = MeasureSpec.getMode(heightMeasureSpec);
setMeasuredDimension((modeWidth == MeasureSpec.EXACTLY) ? mWidth = sizeWidth : mWidth,
(modeHeight == MeasureSpec.EXACTLY) ? mHeight = sizeHeight : mHeight); firstDX = mWidth / - size / ;//第一个图形的水平位置
secondDX = mWidth / - size / ;//...
thirdDX = * mWidth / - size / ;//...
}当有指定了具体值的宽高时,mWidth和mHeight也设置应为测量到的sizeWidth和sizeHeight。
布局
说到布局时先明确一点的是图像的跳动是通过属性动画来控制的,属性动画是什么?我一句话说一下就是:可以以动画的效果形式去更改一个对象的某个属性。还不太了解的可以先找找资料看一下。
布局这里就决定视图里的各种位置的操作了,作为单个控件时一般不怎么用到,我在这里进行动画的初始化并开始的操作了。可以看到我们的BounceProgressBar是三个图形在跳动的。
三个属性的封装如下:
/**
* firstBitmapTop's Property. The change of the height through canvas is
* onDraw() method.
*/
private Property<BounceProgressBar, Integer> firstBitmapTopProperty = new Property<BounceProgressBar, Integer>(
Integer.class, "firstDrawableTop") {
@Override
public Integer get(BounceProgressBar obj) {
return obj.firstBitmapTop;
} public void set(BounceProgressBar obj, Integer value) {
obj.firstBitmapTop = value;
invalidate();
};
};
/**
* secondBitmapTop's Property. The change of the height through canvas is
* onDraw() method.
*/
private Property<BounceProgressBar, Integer> secondBitmapTopProperty = new Property<BounceProgressBar, Integer>(
Integer.class, "secondDrawableTop") {
@Override
public Integer get(BounceProgressBar obj) {
return obj.secondBitmapTop;
} public void set(BounceProgressBar obj, Integer value) {
obj.secondBitmapTop = value;
invalidate();
};
};
/**
* thirdBitmapTop's Property. The change of the height through canvas is
* onDraw() method.
*/
private Property<BounceProgressBar, Integer> thirdBitmapTopProperty = new Property<BounceProgressBar, Integer>(
Integer.class, "thirdDrawableTop") {
@Override
public Integer get(BounceProgressBar obj) {
return obj.thirdBitmapTop;
} public void set(BounceProgressBar obj, Integer value) {
obj.thirdBitmapTop = value;
invalidate();
};
};onLayout部分的代码如下:
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom); if (bouncer == null || !bouncer.isRunning()) {
ObjectAnimator firstAnimator = initDrawableAnimator(firstBitmapTopProperty, speed, size / ,
mHeight - size);
ObjectAnimator secondAnimator = initDrawableAnimator(secondBitmapTopProperty, speed, size / ,
mHeight - size);
secondAnimator.setStartDelay();
ObjectAnimator thirdAnimator = initDrawableAnimator(thirdBitmapTopProperty, speed, size / ,
mHeight - size);
thirdAnimator.setStartDelay();
bouncer = new AnimatorSet();
bouncer.playTogether(firstAnimator, secondAnimator, thirdAnimator);
bouncer.start();
}
} private ObjectAnimator initDrawableAnimator(Property<BounceProgressBar, Integer> property, int duration,
int startValue, int endValue) {
ObjectAnimator animator = ObjectAnimator.ofInt(this, property, startValue, endValue);
animator.setDuration(duration);
animator.setRepeatCount(Animation.INFINITE);
animator.setRepeatMode(ValueAnimator.REVERSE);
animator.setInterpolator(new AccelerateInterpolator());
return animator;
}动画的值变换是从size到mHeight-size的,要减去size的原因是在canvas中,大于(mHeight, mHeight)的左边已经view本身的大小范围了。
绘制
绘制这里做的工作不是很多,就是根据每个图像的水平位置,和通过属性动画控制的高度来去绘制bitmap在画布上。
@Override
protected synchronized void onDraw(Canvas canvas) {
/* draw three bitmap */
firstBitmapMatrix.reset();
firstBitmapMatrix.postTranslate(firstDX, firstBitmapTop); secondBitmapMatrix.reset();
secondBitmapMatrix.setTranslate(secondDX, secondBitmapTop); thirdBitmapMatrix.reset();
thirdBitmapMatrix.setTranslate(thirdDX, thirdBitmapTop); canvas.drawBitmap(firstBitmap, firstBitmapMatrix, mPaint);
canvas.drawBitmap(secondBitmap, secondBitmapMatrix, mPaint);
canvas.drawBitmap(thirdBitmap, thirdBitmapMatrix, mPaint);
}
位置是通过Matrix来控制的,因为当时还考虑到落地的变形,但现在给去掉先了。
总的来说绘制的流程是通过属性动画来控制每个图像在画布上的位置,在属性更改时调用invalidate()方法去通知重绘就行了,看起来就是跳动的效果了,跳动速度的变化则是给动画设置插值器来完成。
完
这篇文章就写到这里了,完整的源码我放到我的github上了(https://github.com/zhengxiaopeng/BounceProgressBar),欢迎大家star、fork一起完善它。
这样我们便完成心形图片的裁剪工作了,得到的bitmap就变成心形了:
这个心可以见人了。。
画完心就该下一步了。
View的绘制
说到view的绘制过程就需要下面三部曲了:
- 测量——onMeasure():决定View的大小
- 布局——onLayout():决定View在ViewGroup中的位置
- 绘制——onDraw():如何绘制这个View。
测量
对于BounceProgressBar控件的测量还是比较简单的,当wrap_content时高度和宽度分别为size的5倍和4倍,其它情况时就指定宽高为具体测量到的值就好。然后决定三个图形在控件之中的水平位置:
Android 自定义View之BounceProgressBar的更多相关文章
- Android自定义View 画弧形,文字,并增加动画效果
一个简单的Android自定义View的demo,画弧形,文字,开启一个多线程更新ui界面,在子线程更新ui是不允许的,但是View提供了方法,让我们来了解下吧. 1.封装一个抽象的View类 B ...
- (转)[原] Android 自定义View 密码框 例子
遵从准则 暴露您view中所有影响可见外观的属性或者行为. 通过XML添加和设置样式 通过元素的属性来控制其外观和行为,支持和重要事件交流的事件监听器 详细步骤见:Android 自定义View步骤 ...
- Android 自定义View合集
自定义控件学习 https://github.com/GcsSloop/AndroidNote/tree/master/CustomView 小良自定义控件合集 https://github.com/ ...
- Android 自定义View (五)——实践
前言: 前面已经介绍了<Android 自定义 view(四)-- onMeasure 方法理解>,那么这次我们就来小实践下吧 任务: 公司现有两个任务需要我完成 (1)监测液化天然气液压 ...
- Android 自定义 view(四)—— onMeasure 方法理解
前言: 前面我们已经学过<Android 自定义 view(三)-- onDraw 方法理解>,那么接下我们还需要继续去理解自定义view里面的onMeasure 方法 推荐文章: htt ...
- Android 自定义 view(三)—— onDraw 方法理解
前言: 上一篇已经介绍了用自己定义的属性怎么简单定义一个view<Android 自定义view(二) -- attr 使用>,那么接下来我们继续深究自定义view,下一步将要去简单理解自 ...
- Android 自定义view(二) —— attr 使用
前言: attr 在前一篇文章<Android 自定义view -- attr理解>已经简单的进行了介绍和创建,那么这篇文章就来一步步说说attr的简单使用吧 自定义view简单实现步骤 ...
- Android 自定义View
Android 自定义View流程中的几个方法解析: onFinishInflate():从布局文件.xml加载完组件后回调 onMeasure() :调用该方法负责测量组件大小 onSizeChan ...
- Android自定义View之CircleView
Android自定义View之CircleView 版权声明:本文为博主原创文章,未经博主允许不得转载. 转载请表明出处:http://www.cnblogs.com/cavalier-/p/5999 ...
随机推荐
- mvn打包发布
一:打包 cmd进入工作目录运行命令 1: mvn clean 2: mvn install 3: mvn clean compile 4: mvn package -DiskipTest ...
- Mac OSX下面的博客客户端Marsedit使用
在windows下面,有一个很好用的博客客户端,叫做windows live writer,不得不感叹,其所见即所得的方面真的是很方便,特别是还可以方便的把word上的内容直接帖上去,包括文件中 ...
- 全 Javascript 的 Web 开发架构:MEAN
http://developer.51cto.com/art/201404/434759.htm 全 Javascript 的 Web 开发架构:MEAN 引言 最近在Angular社区的原型开发者间 ...
- 图形性能(widgets的渲染性能太低,所以推出了QML,走硬件加速)和网络性能(对UPD性能有实测数据支持)
作者:JasonWong链接:http://www.zhihu.com/question/37444226/answer/72007923来源:知乎著作权归作者所有,转载请联系作者获得授权. ---- ...
- java设计模式--创建模式--工厂方法
工厂方法定义: 工厂方法 概述 定义一个用于创建对象的接口,让子类决定实例化哪一个类.FactoryMethod使一个类的实例化延迟到其子类. 适用性 .当一个类不知道它所必须创建的对象的类的时候. ...
- Luci流程分析(openwrt下)
1. 页面请求: 1.1. 代码结构 在openwrt文件系统中,lua语言的代码不要编译,类似一种脚本语言被执行,还有一些uhttpd服务器的主目录,它们是: /www/index.html cgi ...
- Unix/Linux环境C编程入门教程(37) shell常用命令演练
cat命令 cat命令可以用来查看文件内容. cat [参数] 文件名. grep-指定文件中搜索指定字符内容. Linux的目录或文件. -path '字串' 查找路径名匹配所给字串的所有文件 ...
- Best Time to Buy and Sell Stock 解答
Question Say you have an array for which the ith element is the price of a given stock on day i. If ...
- python多线程简单例子
python多线程简单例子 作者:vpoet mail:vpoet_sir@163.com import thread def childthread(threadid): print "I ...
- 2.x ESL第二章习题 2.8
题目 代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 3 ...