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那么它一起。

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

    时间: 2024-10-22 09:21:06

    Android 它们的定义View它BounceProgressBar的相关文章

    【Android】自己定义View、画家(画布)Canvas与画笔Paint的应用——绘图、涂鸦板app的实现

    利用一个简单的绘图app来说明安卓的图形处理类与自己定义View的应用. 例如以下图,有一个供用户自己随意绘图.涂鸦的app. 这里不做那么花俏了,仅提供黑白两色.但能够改变笔尖的粗细. 实质上这里的橡皮擦就是白色的画笔,根本不用使用到画笔的setXfermode方法,要搞一堆复杂的project. 用户画完图之后能够保存图像.图像的文件名称是当前的时间.保存的位置是sdcard的根文件夹. 制作步骤例如以下: 1.先设置好字体文件res\values\strings.xml,主要是app的名称

    【Android】自己定义View、画布Canvas与画笔Paint

    安卓自己定义View事实上非常easy. 这个View能够像<[Android]利用Java代码布局,button加入点击事件>(点击打开链接)一样.利用Java代码生成一系列的组件. 也能够配合画布Canvas与画笔Paint来使用. 以下用一个样例来说明.例如以下图,有一个自己定义布局View.里面摆放着,利用画布Canvas与画笔Paint绘制出来的蓝色正方形与红色文字. 在res\layout\activity_main.xml中.直接像摆放安卓固有组件一样,能够直接使用这个我定义组件

    Android 它们的定义View

    安卓开发过程,安卓官方控制有时来自往往不能满足我们的需求.这一次,我必须定义自己.下面我们就来看看他们的定义View: package com.example.myview; import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Paint.Style;

    Android 开发 -------- 自己定义View 画 五子棋

    自己定义View  实现 五子棋 配图: 代码: package com.example.fiveson; import java.util.LinkedList; import java.util.List; import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.uti

    【Android】自己定义View

    翻译自:http://developer.android.com/training/custom-views/index.html 一)创建view类 一个设计良好的自己定义view与其它的类一样.它使用接口来封装一系列的功能.有效的使用CPU和内存等.除了这些,定制view还应该满足例如以下条件: 符合Android标准 与Android XML 布局文件配合,提供符合style风格的定制属性 发送易接近性事件(accessibility events,针对听力或视觉有缺陷用户提供方便的事件)

    Android 它们的定义View视图

    创建一个新视图将满足我们独特UI需求. 本文介绍的发展将指南针罗盘接口使用UI,通过继承View定义自己的视图类工具,为了深入了解自己的自定义视图. 实现效果图: 源码: 布局文件activity_main(当中CompassView继承View类): <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.co

    手把手带你画一个 时尚仪表盘 Android 自己定义View

    拿到美工效果图.咱们程序猿就得画得一模一样. 为了不被老板喷,仅仅能多练啊. 听说你认为前面几篇都so easy,那今天就带你做个相对照较复杂的. 转载请注明出处:http://blog.csdn.net/wingichoy/article/details/50468674 注意:每一篇博客都是建立在之前博客的基础知识上的,假设你刚接触自己定义view.能够来说说自己定义view简单学习的方式这里看我曾经的文章.记录了我学习自己定义view的过程,并且前几篇博客或多或少犯了一些错误(反复绘制,o

    【android自己定义控件】自己定义View属性

    1.自己定义View的属性 2.在View的构造方法中获得我们自己定义的属性 3.重写onMesure 4.重写onDraw 3这个步骤不是必须,当然了大部分情况下还是须要重写的. 1.自己定义View的属性,首先在res/values/  下建立一个attrs.xml , 在里面定义我们的属性和声明我们的整个样式. <?xml version="1.0" encoding="utf-8"?> <resources> <attr nam

    Android自己定义View基础篇(三)之SwitchButton开关

    自己定义View基础篇(二) 自己定义View基础篇(一) 自己定义View原理 我在解说之前,先来看看效果图,有图有真相:(转换gif图片效果太差) 那来看看真实图片: 假设你要更改样式,请改动例如以下图片: switch_ball switch_bg switch_black switch_bottom 我在这里就不反复解说View与ViewGroup的关系,View的绘制流程.假设你对自己定义View还不甚了解.请看上面几篇文章. 用法 xml文件: <com.github.ws.swit