Android 它们的定义View它BounceProgressBar

转载请注明出处:http://blog.csdn.net/bbld_/article/details/41246247 【Rocko's blog


之前几天下载了非常久没用了的桌面版酷狗来用用的时候,发现当中载入歌曲的等待进度条的效果不错(个人感觉)。例如以下:




然后趁着这周末两天天气较冷,窝在宿舍放下成堆的操作系统作业(目測要抄一节多课的一堆堆文字了啊...啊..)毅然决定把它鼓捣出来,终于的效果例如以下(总感觉有点不和谐啊·):




对照能看出来的就是多了形状的选择还有使用图片了。那么接下来就是它的实现过程。

对自己定义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="1" /> --> <!-- the progress child shape --> <attr name="shape" format="enum"> <enum name="original" value="0" /> <enum name="circle" value="1" /> <enum name="pentagon" value="2" /> <enum name="rhombus" value="3" /> <enum name="heart" value="4" /> </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="250"
            bpb:src="#6495ED" />
自己定义了属性最后我们要做的就是在代码里去获取它了,在哪里获取呢,当然是BounceProgressBar类的构造方法里了,相关代码例如以下:
	public BounceProgressBar(Context context) {
		this(context, null, 0);
	}

	public BounceProgressBar(Context context, AttributeSet attrs) {
		this(context, attrs, 0);
	}

	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, 250);
		size = a.getDimensionPixelSize(R.styleable.BounceProgressBar_singleSrcSize, 50);
		shape = a.getInt(R.styleable.BounceProgressBar_shape, 0);
		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)也是能够的。

第二个參数是默认值。


图形的形状

得到属性值后。我们就能够去做对应的处理操作了,这里是图形形状的获取,用到了shapesrcsize属性。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(0, 0, 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(0, 0, 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 / 2, 0);
			path.lineTo(0, size / 2);
			path.lineTo(size / 2, size);
			path.lineTo(size, size / 2);
			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 * 36 / 180);
			float radius = size / 2;
			// In the middle of the radius of the pentagon
			float radius_in = (float) (radius * Math.sin(radian / 2) / Math.cos(radian));
			// The starting point of the polygon
			path.moveTo((float) (radius * Math.cos(radian / 2)), 0);
			path.lineTo((float) (radius * Math.cos(radian / 2) + radius_in * Math.sin(radian)),
					(float) (radius - radius * Math.sin(radian / 2)));
			path.lineTo((float) (radius * Math.cos(radian / 2) * 2),
					(float) (radius - radius * Math.sin(radian / 2)));
			path.lineTo((float) (radius * Math.cos(radian / 2) + radius_in * Math.cos(radian / 2)),
					(float) (radius + radius_in * Math.sin(radian / 2)));
			path.lineTo((float) (radius * Math.cos(radian / 2) + radius * Math.sin(radian)),
					(float) (radius + radius * Math.cos(radian)));
			path.lineTo((float) (radius * Math.cos(radian / 2)), (float) (radius + radius_in));
			path.lineTo((float) (radius * Math.cos(radian / 2) - radius * Math.sin(radian)),
					(float) (radius + radius * Math.cos(radian)));
			path.lineTo((float) (radius * Math.cos(radian / 2) - radius_in * Math.cos(radian / 2)),
					(float) (radius + radius_in * Math.sin(radian / 2)));
			path.lineTo(0, (float) (radius - radius * Math.sin(radian / 2)));
			path.lineTo((float) (radius * Math.cos(radian / 2) - radius_in * Math.sin(radian)),
					(float) (radius - radius * Math.sin(radian / 2)));
			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 / 4, 0, size - (size / 4), size);
   path.addOval(ovalRect, Path.Direction.CW);
2、旋转图形。大概45度左右

   matrix.postRotate(42, size / 2, size / 2);
   path.transform(matrix, path);


3、选取旋转后的右半部分图形,并用cancas画出这半边的心形

			path.transform(matrix, path);
			region.setPath(path, new Region((int) size / 2, 0, (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(-42, size / 2, size / 2);
			path.transform(matrix, path);
			region.setPath(path, new Region(0, 0, (int) size / 2, (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 / 4 - size / 2;//第一个图形的水平位置 secondDX = mWidth / 2 - size / 2;//... thirdDX = 3 * mWidth / 4 - size / 2;//... }

    当有指定了详细值的宽高时,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 / 2,
    					mHeight - size);
    			ObjectAnimator secondAnimator = initDrawableAnimator(secondBitmapTopProperty, speed, size / 2,
    					mHeight - size);
    			secondAnimator.setStartDelay(100);
    			ObjectAnimator thirdAnimator = initDrawableAnimator(thirdBitmapTopProperty, speed, size / 2,
    					mHeight - size);
    			thirdAnimator.setStartDelay(200);
    			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那么它一起。       

  • 版权声明:本文博客原创文章。博客,未经同意,不得转载。

    posted @ 2015-08-20 09:42  hrhguanli  阅读(291)  评论(0编辑  收藏  举报