Android好奇宝宝_番外篇_看脸的世界_06

来源:互联网 发布:linux qstat查询结果 编辑:程序博客网 时间:2024/06/10 01:46
简单实现波纹效果



其实这一篇的效果实现很简单,写这篇博客重点是为了说另一件事,剧透一下:有关内存泄露的。

先说下效果的实现:

原理:
原理只有一个,就是Shader的使用。Shader我看别人翻译成着色器,其实它的作用就是为画笔增加颜色的渐变,画笔默认是一个颜色画到底,但是使用Shader可以实现从一个颜色渐变到另一个颜色。

想了解更多关于Shader的姿势,推荐博客:传送门

有了Shader,就能很简单的画出波纹的效果了,至于动画效果,只是动态改变画的大小而已。

高清源码:

(1)初始化
重写构造方法:
public BoWenView(Context context, AttributeSet attrs) {super(context, attrs);init();}

进行必要的初始化:
private void init() {// 画笔初始化mPaint = new Paint();mPaint.setAntiAlias(true);// 发送消息,开始动画循环mHandler.sendEmptyMessage(0);}

下面是一些需要View大小作为参数的初始化,我是重写了onSizeChanged方法,该方法顾名思义就是View的大小被改变时会被调用,第一次加入View树时也会被调用,只是此时的旧值都为0而已:
protected void onSizeChanged(int w, int h, int oldw, int oldh) {super.onSizeChanged(w, h, oldw, oldh);// 计算半径=较长边的1/12加1radius = w > h ? w / 12 + 1 : h / 12 + 1;// 计算中心坐标cX = w / 2;cY = h / 2;// 初始化环形着色器radialGradient = new RadialGradient(w / 2, h / 2, radius, 0X88FFFFFF, 0XAAFFFFFF, TileMode.REPEAT);mPaint.setShader(radialGradient);// 初始化中心图像的位置bitmapRectF = new RectF(cX - radius, cY - radius, cX + radius, cY + radius);}

要实现我们的效果需要用的是环形着色器,另外还有线性的和圆形的,更多内容可以查看上面推荐的博客。

简单说下RadialGradient的构造参数:
RadialGradient(x, y, h, color0, color1, tile)
x和y为圆心坐标,h为渐变的半径,这里设置为我们计算出来的小半径,配合我们设置了TileMode.REPEAT模式,才会有上面一圈一圈的效果,如果h设置成了View的宽或者高,那么显示出来的只有一圈。至于TileMode有3种模式,有兴趣的可以试下不同模式下效果的区别。color0是开始颜色,会渐变到终止颜色color1。这里都是设成白色,不过透明度不同。

(2)中心图像
提供一个方法给外部设置要显示的中心图像,然后再onDraw方法里画出来就行了,至于位置已经在前面初始化时计算出来了:
private Bitmap centerBitmap;public void setCenterBitmap(Bitmap bitmap) {centerBitmap = bitmap;invalidate();}


(3)动画实现
前面说过动画只是动态改变画的圆大小而已,这里用一个Handler来循环发送消息:
Handler mHandler = new Handler(new Handler.Callback() {@Overridepublic boolean handleMessage(Message msg) {Log.e("Bowen", "handleMessage");if (scale >= 6) {scale = 0;}scale++;invalidate();mHandler.sendEmptyMessageDelayed(0, 500);return false;}});

通过scale的值来控制要画的圆的大小。


(4)开始画
protected void onDraw(Canvas canvas) {//画圆,根据sacle来控制大小canvas.drawCircle(cX, cY, cX > cY ? cX * scale / 6 : cY * scale / 6, mPaint);//有设置图像的话就在中间画出来if (centerBitmap != null) {canvas.drawBitmap(centerBitmap, null, bitmapRectF, null);}}

好了,写完了,放到Activity实验一下:
protected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);//设置中心图像((BoWenView) findViewById(R.id.bowen)).setCenterBitmap(BitmapFactory.decodeResource(getResources(),R.drawable.dun));}

效果出来了,万事大吉,撸两盘然后睡觉去了。

手机还显示着Demo的Activity,点击Home键返回桌面。就在这时,恐怖的事情发生了,Logcat还一直打印着handleMessage方法里的打印信息。这意味着Handler没有被回收,Handler是BowenView的成员变量,Handler无法被回收意味着BowenView也无法被回收,而BowenView又拥有Activity的引用,意味着整个Activity在应用被真正关闭之前(即进程被销毁之前)都无法回收,这TM的不就是内存泄露嘛,我居然造了一颗炸弹。

一个常犯的错误:

其实在Activity中使用Handler一样有这个问题,发生这个问题的原因是:

先理解下面两句话再继续往下看:
(1)基于垃圾回收规则:只回收没有引用的对象。
(2)在Java中:非静态(匿名)内部类会引用外部类对象。

当你发送一个延时消息时,这个消息会被添加到一个全局的消息队列中,当时间到达要延时的时间时,这个消息会被发送会给我们的Handler。(对该Handler机制不熟悉的,请参考我另一篇博客:传送门)

如果在中间过程中,我们退出了Activity,那么当垃圾收集器准备回收这个Activity时,发现在全局消息队列中还有一个消息里有着一个指向该Handler的引用,于是该Handler无法被回收。

如果是像上面那种情况,Handler是View的成员变量,即Handler引用了View,而View引用了Activity,Activity引用了一大把东西,所以有一大把东西会占着内存,而且我们还访问不到,垃圾收集器也回收不了。

如果是在Activity中使用Handler也是同样的道理,即Handler为Activity的非静态内部类,即Handler引用了Activity,即Handler的无法回收同样将导致Activity的无法回收。

如果这个Activity很占内存,那么内存泄露堆积,就很容易造成OOM,而且原因也很难排查得到。
(从破坏的角度看,我做的还是挺不错的)


解决方法:
其实我的发现是基于这篇博客:传送门
这篇博客也提供了一种解决方法,原理是将Handler写为静态类,并且在Handler内部保存了一个Activity的软引用。

下面是我的解决方法:

(1)在View中使用Handler:
可以在View类中重写onWindowVisibilityChanged方法,当Activity所属的窗口变为不可见时,从消息队列中移除我们发送的消息,代码修改如下:
protected void onWindowVisibilityChanged(int visibility) {super.onWindowVisibilityChanged(visibility);if (visibility == View.GONE) {Log.e("Bowen", "Window-GONE");mHandler.removeMessages(0);} else if (visibility == View.VISIBLE) {Log.e("Bowen", "Window-VISIBLE");mHandler.sendEmptyMessage(0);}}

记得把前面在init方法中发送消息的语句去掉:
private void init() {// 画笔初始化mPaint = new Paint();mPaint.setAntiAlias(true);// 发送消息,开始动画循环//mHandler.sendEmptyMessage(0);}

2)在Activity中使用Handler:
与上面类似,重写onStop方法,在onStop方法中移除消息。
//我就是传说中的//略...



Demo下载

0 0
原创粉丝点击