首页 > 代码库 > android之View和LinearLayout的重写(实现背景气泡和波纹效果)
android之View和LinearLayout的重写(实现背景气泡和波纹效果)
前两天看了仿android L里面水波纹效果的两篇博客
Android L中水波纹点击效果的实现
Android自定义组件系列【14】——Android5.0按钮波纹效果实现
第一篇是实现了一个水波纹布局,放在里面的所有控件点击后都会出现波纹效果
第二篇是实现了一个水波纹view,点击之后自身会出现波纹效果
根据对这两篇博客的理解,我自己实现了一个类似的东西,没找到合适的录屏软件,只好把波纹的速度调快了很多才录下来,能看出来啥意思,不调速度的话还算比较优雅。
就是像上面这样一个控件,里面的背景用的是一个重写的TextView,背景就一直有一个不断“呼吸”的气泡。
这里就联系前面两篇博客(建议先去看下),介绍下在View和ViewGroup中实现背景动画,同时纪录下自己对里面知识点的理解。
就暂时把这种效果命名成会呼吸的气泡。(后来发现这个图不动,好吧不知为啥不浪费时间了,就自己想象下这个整个背景有一个圆,不停的放大缩小放大缩小,圆心随机,这个刚好是随机到左上角位置了)
首先是ViewGroup
这里就直接以第一篇博客为例,纪录下自己的理解
1.获取坐标,这个是比较重要的一步,之后判断点击的控件还有绘制波纹都需要用到
@Override protected void onLayout(boolean changed, int l, int t, int r, int b) { super.onLayout(changed, l, t, r, b); this.getLocationOnScreen(mLocationInScreen); }
这里获取到的是当前布局左上角在整个屏幕中的坐标
2.获取点击到的控件,这里的获取就需要根据坐标挨个判断了,在dispatchTouchEvent方法里面会传入当前点击事件MotionEvent,他会带入当前点击的坐标,这里有两种获取方式,一种是getX,一种是getRawX,在view的坐标系里面说到这两种的区别了,为了计算点击事件,这里要获取的是相对屏幕的坐标,其实在布局的重写里面整片都是使用相对屏幕的坐标,因为布局会出现嵌套,嵌套之后相对坐标就不对了,所以全都使用相对屏幕的坐标去计算。获取到点击事件的坐标,就可以拿坐标去找到所点击的控件了。整个过程博客里面已经很详细了。
3.拿到点击的控件之后就要在控件上面绘制波纹了,这里代码还是贴一下
// view绘制流程:先绘制背景,再绘制自己(onDraw),接着绘制子元素(dispatchDraw),最后绘制一些装饰等比如滚动条(onDrawScrollBars) // 为了防止绘制绘制的子元素把波纹挡住,这里选择在子元素绘制完成再绘制波纹 @Override protected void dispatchDraw(Canvas canvas) { super.dispatchDraw(canvas); if (!mShouldDoAnimation || mTargetWidth < 0 || mTouchTarget == null) { return; } if (mRevealRadius > mMinBetweenWidthAndHeight / 2) {// 当半径超过短边之后,增加扩散速度尽快完成扩散 mRevealRadius += mRevealRadiusGap * 4; } else {// 波纹当半径递增扩散 mRevealRadius += mRevealRadiusGap; } this.getLocationOnScreen(mLocationInScreen);// 获取本布局的坐标---1?? int[] location = new int[2]; mTouchTarget.getLocationOnScreen(location);// 获取点击控件的坐标---2?? int left = location[0] - mLocationInScreen[0];//---3?? int top = location[1] - mLocationInScreen[1]; int right = left + mTouchTarget.getMeasuredWidth(); int bottom = top + mTouchTarget.getMeasuredHeight(); canvas.save(); canvas.clipRect(left, top, right, bottom);//---4?? canvas.drawCircle(mCenterX, mCenterY, mRevealRadius, mPaint); canvas.restore(); if (mRevealRadius <= mMaxRevealRadius) { postInvalidateDelayed(INVALIDATE_DURATION, left, top, right, bottom);//---5?? } else if (!mIsPressed) { mShouldDoAnimation = false; postInvalidateDelayed(INVALIDATE_DURATION, left, top, right, bottom); } }
整个绘制的过程就是上面这样,还是重点关注下画布的切割和坐标的计算,绘制流程和半径的计算之类的看看就明白了
1??获取的是布局左上角的坐标
2??获取的是控件左上角的坐标
3??控件的坐标减去布局的坐标,就是布局在控件上的相对坐标,这里的相对坐标其实就是getLeft和getTop的概念,但是不能这么用,因为有可能控件与布局之间还有嵌套别的布局
4??在3??中已经获取到了控件相对于布局的坐标,这里就在布局的画布上把控件对应的位置切割下来,然后在上面画圆,切割是为了提高性能
5??这里画完圆之后要马上画下一个半径更大的圆,从而达到扩散的效果,所以要postInvalidateDelayed去刷新,刷新的时候只刷新控件所对应的那个区域,也是为了提高性能
基本上就这些吧,原博已经讲得比较详细了,我这里只是针对自己的理解纪录下。
然后是View
看一下分几个步骤
1.获取当前控件的宽高信息(用来初始化气泡的半径等信息)
2.获取点击事件(用来作为气泡的圆心,在没有点击事件的时候它是随机坐标作为圆心的,点击则移动到点击的位置)
3.绘制气泡
下面是重写的BreathTextView代码
public class JasonBreathTextView extends TextView { private JasonBreathCircle breathCircle; public JasonBreathTextView(Context context) { super(context); } public JasonBreathTextView(Context context, AttributeSet attrs) { super(context, attrs); breathCircle = new JasonBreathCircle(context); } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { // TODO Auto-generated method stub super.onSizeChanged(w, h, oldw, oldh); breathCircle.initParameters(this); } @Override public boolean onTouchEvent(MotionEvent event) { breathCircle.setCircleCenter((int) event.getX(), (int) event.getY()); return super.onTouchEvent(event); } @Override protected void onDraw(Canvas canvas) { breathCircle.draw(canvas); super.onDraw(canvas); } /** * 开始水纹效果 */ public void startReveal() { breathCircle.start(); } /** * 停止水纹效果 */ public void stopReveal() { breathCircle.stop(); } }
这个就是图中使用的继承自TextView的控件。
为了使用方便,我把气泡的实现和控件的实现分开了,这样之后不管实现哪种View都可以直接使用分离出来的气泡类,使用方法就像上面这样,只需要把view控件本身传给气泡,气泡就会在控件上绘制了。
气泡BreathCircle的代码如下:
public class JasonBreathCircle { private static int DIFFUSE_GAP = 2; // 扩散半径增量 private static final int INVALIDATE_DURATION = 10; // 每次刷新的时间间隔 private Context mContext; private boolean needToDrawReveal = false;// 绘画标志位 // 圆形自身的一些属性 private boolean isLargerMode = true;// 呼吸模式 private Paint mPaintReveal;// 画笔 private int mCircleCenterX;// 圆心x private int mCircleCenterY;// 圆心y private int mCurRadius;// 当前半径 private int mMaxRadius;// 最大半径 // 依附的控件的一些属性,利用高度宽度计算当前触摸点的位置 private View mParentView;// 依附的控件 private int mParentHeight;// 控件高度 private int mParentWidth;// 控件宽度 // ================初始化方法(必须调用)=============== /** * 实例化一个圆,之后要调用initParameters初始化该圆的属性,再之后就可以draw了 * * @param context */ public JasonBreathCircle(Context context) { mContext = context; initPaint(); } /** * 传入view,用来初始化坐标,半径,默认以中心为圆心开始画圆 * * @param view */ public void initParameters(View view) { this.mParentView = view; // 获取当前依附控件的属性 mParentHeight = mParentView.getHeight(); mParentWidth = mParentView.getWidth(); // 初始化圆的属性 mMaxRadius = (int) Math.hypot(view.getHeight(), view.getWidth()) / 2; // 控件的宽度高度求出初始圆心 mCircleCenterX = mParentWidth / 2; mCircleCenterY = mParentHeight / 2; } /** * 传入画布 * * @param canvas */ public void draw(Canvas canvas) { if (needToDrawReveal) { canvas.save(); canvas.drawCircle(mCircleCenterX, mCircleCenterY, mCurRadius, mPaintReveal); canvas.restore(); if (isLargerMode && mCurRadius < mMaxRadius) { mCurRadius += DIFFUSE_GAP;// 波纹递增 postRevealInvalidate(); } else if (mCurRadius > 0 && !isLargerMode) { // 画完一个周期从头再画 mCurRadius -= DIFFUSE_GAP;// 波纹递增 postRevealInvalidate(); } else {// 转换模式 isLargerMode = !isLargerMode; // 随机选择坐标作为圆心,从0到最右边中间取x,从0到底边取y setCircleCenter(JasonRandomUtil.nextInt(0, mParentWidth), JasonRandomUtil.nextInt(0, mParentHeight)); // 圆心更换后,缩小前,把当前半径设为最大,防止边上出现空白覆盖不满 if (!isLargerMode) { mCurRadius = mMaxRadius; } postRevealInvalidate(); } } } // ===============对外接口=============== /** * 开始呼吸 */ public void start() { if (needToDrawReveal) { return; } needToDrawReveal = true; postRevealInvalidate(); } /** * 停止呼吸 */ public void stop() { if (!needToDrawReveal) { return; } needToDrawReveal = false; reset(); postRevealInvalidate(); } /** * 设置圆心 * * @param x * @param y */ public void setCircleCenter(int x, int y) { mCircleCenterX = x; mCircleCenterY = y; mMaxRadius = JasonRadiusUtil.getMaxRadius(mCircleCenterX, mCircleCenterY, mParentWidth, mParentHeight); } /** * 设置画圆为空心还是实心,默认实心 * * @param isHollow */ public void setHollow(boolean isHollow) { mPaintReveal.setStyle(isHollow ? Paint.Style.STROKE : Paint.Style.FILL); } // ================内部实现=============== /** * 重置 */ private void reset() { mCurRadius = 0; isLargerMode = true; } /** * 初始化画笔 */ private void initPaint() { mPaintReveal = new Paint(); mPaintReveal.setColor(mContext.getResources().getColor( R.color.jason_bg_common_green_light)); mPaintReveal.setAntiAlias(true); } /** * 重绘 */ private void postRevealInvalidate() { mParentView.postInvalidateDelayed(INVALIDATE_DURATION); } }
获取控件的宽高信息是在onSizeChanged这个方法中,这里会有宽高信息可以去获取
获取点击事件是在onTouchEvent里面
最后绘制这里选择了onDraw方法
如果看过前面两篇博客了,这里还是纪录了两个地方:
1??关于绘制其实有好几个方法可以选用,看下view绘制流程:先绘制背景,再绘制自己(onDraw),接着绘制子元素(dispatchDraw),最后绘制一些装饰等比如滚动条(onDrawScrollBars)
因为这里是要把绘制出来的气泡做背景,所以要在气泡绘制完成才去绘制view自身的一些东西,所以在onDraw里面最合适了
2??关于坐标,由于受前面第一篇博客里面布局重写时对坐标处理的影响,在这里浪费了些时间,其实在view的重写里面,重绘的时候使用坐标,只需要知道view的宽高就够了,因为onDraw传进来的canvas就是控件本身的大小,
所以不需要像布局里面那样对画布进行裁剪,只要直接在上面画就行了。坐标系直接自己按照宽高去建立就行了,左上角是原点。
作者:jason0539
微博:http://weibo.com/2553717707
博客:http://blog.csdn.net/jason0539(转载请说明出处)
android之View和LinearLayout的重写(实现背景气泡和波纹效果)