频道栏目
首页 > 程序开发 > 移动开发 > 其他 > 正文
自定义基于VolleyNetworkImageView的圆形网络请求图片控件
2016-10-09 09:42:42      个评论    来源:qq_28899635的博客  
收藏   我要投稿

今天我要记录的问题是,如何基于Volley的NetworkImageView自定义一个圆形的带网络请求功能的View控件。Volley本身只是个网络请求库,图片请求功能可以说是附带功能,虽然在大多数简单的情况下够用,但是难以应对复杂的情况。举例来说,你没法控制图片的形状,服务器给你返回的图片是什么形状,你就只能用什么形状,你没法实现一个框框,让网络图片加载过来以后剪裁成框框的形状,就算你跟服务器程序员商量好,他返回的图片形状都能合你的意,但是你的显示图片的控件仍然是矩形,只是中间的内容符合条件罢了。如果你用Fresco或者Glide,这样的问题迎刃而解,无论圆的方的,还是带圆角的都能轻松实现,这就是专业的图片请求库的强大之处。但是作为一个懒人,只是为了解决眼前的一个小问题,我也懒得再拉一个Fresco进来,就索性决定自己实现。

首先,我们得要一个圆形图片控件,圆形图片控件官方API是不提供的,大家基本用的都是网上开源的,但是版本众多,有些自定义的圆形ImageView很多细节没处理好,可能会遇到各种问题,比如说抗锯齿也许不到位等等。我在掘金上找到一篇文章,文章里写的CircleImageView据作者描述具有很好的抗锯齿的特点,于是我决定采用他的代码,至于作者的源码以及讲解大家可以看他的原文:Android 自定义控件之 CircleImageView

直接在项目中新建一个类,把作者的源码考进来就行了。然后为了让这个CircleImageView有网络请求功能,我们需要修改一些代码,比如第一步把CircleImageView继承的父类从系统提供的ImageView改成Volley的NetworkImageView(NetworkImageView的父类也是ImageView),这样,CircleImageView瞬间就具备了网络请求功能,这样做其实是很投机取巧的,所以果然,在后续的使用中遇到了很多坑。首先第一个问题,不给控件指定src,直接就会崩溃:我们本来想的是从网络加载图片然后显示,这样的话,本来没必要给控件指定一个本地的src,但是实际上是行不通的。报错以后我们找到问题的源头是CircleImageView中的onMeasure流程,先贴一段onMeasure的部分源码:

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int intrinsicWidth = getDrawable().getIntrinsicWidth();
        int intrinsicHeight = getDrawable().getIntrinsicHeight();
        Log.d(TAG, "intrinsicWidth = " + intrinsicWidth + " intrinsicHeight = " + intrinsicHeight);
        if (isCircle) {
            /**
            *1、如果圆形半径设置为0,则使用图片中宽高之间的最小值作为圆形的半径
            *2、如果圆形半径不为0,则取半径,宽,高之间的最小值作为半径
            **/
            int width = resolveAdjustedSize(radius == 0 ? intrinsicWidth : Math.min(intrinsicWidth, radius * 2), Integer.MAX_VALUE, widthMeasureSpec);
            int height = resolveAdjustedSize(radius == 0 ? intrinsicHeight : Math.min(intrinsicHeight, radius * 2), Integer.MAX_VALUE, heightMeasureSpec);

            int border = Math.min(width, height);

            radius = border / 2;

            Log.d(TAG, "isCircle border = " + border + " radius = " + radius);
            setMeasuredDimension(border, border);

可以看到,onMeasure流程首先就要获取drawable的固定宽高,再通过和使用者设置的半径的大小判断来决定最终的测量半径。因此如果不设置src,getDrawable()只能返回null,所以NullPointerException就会报出,为了简便起见,在使用时先设置一个本地src就能避免这个坑,反正网络图片在加载出来之前是要显示一张图片的,所以在这里直接设置一张也未尝不可。除此之外还有个小细节,NetworkImageView是一个能自动将网络加载的图片压缩成指定大小的控件,所以我们在使用时,半径多数情况下是自己指定的,而不是根据设定的src的宽高来得到半径,因此我们不用CircleImageView代码作者提供的半径选取方案,而是只要半径不为0,就直接将设定的半径作为最终半径,这个代码修改很简单,我就不贴代码了。

注意以上问题以后在普通的使用中就没有什么问题了,但是使用场景不会总是仅仅把图片显示到界面上这么简单,有时候还会遇到更复杂的使用场景,比如RecyclerView,这时候以上代码就会遇到新的坑。

这就是第二个问题,图片资源回收后从缓存中重加载。我写了一个文章评论的模块,打开某一篇文章的评论列表,就会看到各个用户对文章的评论,当然,它会显示每个评论人的头像,于是我的做法是这样的:首先定义一个评论实体类,Comment,其中,Comment中有一个字段,ImageLoader,对,就是Volley中使用NetworkImageView的setImageUrl时必须传入的参数之一,这样每一个Comment的对象都有一个自己的ImageLoader,就是加载每条评论的评论人头像必备的。然后setImageUrl方法放在adapter中的onBindViewHolder方法中执行,这样,在RecyclerView的每个item中显示一张图片的逻辑就写完了。但是测试的时候,把评论列表拉到下面,再拉回最上面的位置的时候,程序又崩溃报错了,查了一下错误日志,发现报错的方法是CircleImageView中的drawableToBitmap方法,我把这个方法的源码贴出来:

private Bitmap drawableToBitmap(Drawable drawable) {
        int w = drawable.getIntrinsicWidth();
        int h = drawable.getIntrinsicHeight();
        Bitmap bitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
        Canvas canvas = new Canvas(bitmap);
        drawable.draw(canvas);
        return bitmap;
    }
代码很短,从名字就能看出来,这个方法是用来把Drawable对象转化为Bitmap的。那它在什么地方调用呢?调用它的两个方法是两种不同的绘制方式的执行流程,关于CircleImageView的两种绘制方式代码作者的原文里有介绍,这里就不重复了。无论采用什么绘制方式,最终都是在onDraw中执行,也就是自定义View的三大流程中的最后一个绘制流程。

错误日志显示的异常是错误的参数传递,宽和高必须都大于0

也就是drawableToBitmap这个方法的第四行,初始化Bitmap的时候中传入的参数w和h没有都大于0。这个原因是什么呢,了解了RecyclerView的机制以及我们刚才测试的时候的操作方式我们就可以知道:在RecyclerView中显示的图片只要移出了屏幕,图片资源就会被回收掉,当再次从屏幕中拉回来的时候,图片资源会被重新加载(这样的机制是为了避免多图OOM),也就是说onBindViewHolder方法在对应的item每次进入屏幕的时候都会执行一次。如果item中的ImageView显示的是本地图片,在重加载的时候就会重新拉出图片来显示,如果显示的是网络图片,由于做了缓存,重加载的时候就会从缓存中将图片拿出来,如果缓存不存在,则会再次发送一次网络请求(之所以会这样执行,也是因为我们把NetworkImageView的setImageUrl方法放在了onBindViewHolder中执行,这个前面已经说过了)。RecyclerView回收图片只是回收图片的内容,而不是把图片这个控件销毁,所以当item中的图片被回收掉又重新加载的时候,CircleImageView的onMeasure和onLayout都不会再次执行,会执行的只有最后一个流程onDraw。所以问题就出在这个onDraw上,前文我们讲过,如果不给CircleImageView指定src的话,程序就会崩溃,原因就是第一个流程onMeasure执行的时候第一步就是获取到图片的drawable,并获得它的固定宽高从而完成onMeasure流程,由于我们指定了src,所以获取到的drawable就不会为空,因此这个问题自然也就解决了,但是,成功完成了CircleImageView的三大流程之后我们又使用setImageUrl方法从网络加载了图片资源并显示在CircleImageView上,这样的话,原本指定的src所对应的drawable对象就不存在了,这时候显示的是从网络加载过来的图片,但这个图片一直是以Bitmap的形式存在,因此当图片资源被回收又重加载的时候,drawableToBitmap方法中的w和h想通过获取drawable的固定宽高的方式来获得它们自己的值得方式就行不通了,所以这里w和h获得的宽高都是不合法的,因此Bitmap的构建也就是不成功的。我们整理一下思路,item中的图片被回收,现在要重新被加载,那首先我们应该考虑的就是把刚才缓存的拿出来直接用,那缓存的图片放在哪里了呢,这时候我们就应该阅读Volley NetworkImageView的源码,上一篇文章已经分析过,请求是通过loadImageIfNecessary来实现的,它有一系列的判断,比如上篇文章讲过的,URL地址重复则不发送请求,以及这次我们要关注的缓存如果存在则直接使用缓存的图片等等,上篇文章我也提到过,加载的具体实现还是ImageLoader,因此我们这里也应该去阅读ImageLoader的源码,具体的源码我也不贴了,实在是太长了,我直接说结论,ImageLoader有个内部类ImageContainer,这个我之前也提到了不少,它有一个字段mBitmap,通过阅读后文可知,从网络加载过来的图片就存放在mBitmap里,刚好ImageContainer提供了一个方法getBitmap()来返回mBitmap,现在我们要在NetworkImageView中拿到这个mBitmap,只需定义一个方法:

    public Bitmap returnBitmap() {
        return mImageContainer.getBitmap();
    }
这样就OK了,mImageContainer是NetworkImageView所持有的字段。现在我们要修改CircleImageView的drawableTOBitmap方法了,由于CircleImageView直接继承自NetworkImageView,所以我们可以直接在drawableToBitmap方法里调用returnBitmap()这个方法。

因此drawableToBitmap方法的代码被改成如下形势:

   private Bitmap drawableToBitmap(Drawable drawable) {
        int w = drawable.getIntrinsicWidth();
        int h = drawable.getIntrinsicHeight();
        Bitmap bitmap;
        if (w <= 0 || h <= 0) {
            bitmap = returnBitmap();
            }
        } else {
            bitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
        }
        Canvas canvas = new Canvas(bitmap);
        drawable.draw(canvas);
        return bitmap;
    }

这样看起来就行了,于是我们打开app再测试一下。开始试了几次都成功通过测试,但是后面只要滑动RecyclerView的速度够快,程序一样会崩溃。

于是我们再来分析这次崩溃的原因,它说构建Canvas的时候的实参bitmap是个null。于是我仔细分析了自己的代码,第一次onDraw执行的时候w和h都是本地指定的src的,因此bitmap对象是通过Bitmap.createBitmap()方法构建的,重加载的时候,也就是if判断中w和h都不正常的时候我们通过returnBitmap把缓存的Bitmap拿出来直接用。那还有哪一种情况没有考虑到呢?我仔细分析之后终于知道了,当进入评论列表的界面的时候,由于滑动速度太快,有些item的图片还没有从网络加载完成,我们就把它滑出了屏幕,这时候它就被回收了,这种情况下,由于网络图片没有加载完成,所以缓存是不存在的,而CircleImageView显示的图片是本地的src指定的图片,所以这个图片在这里被回收了,但是重加载的时候我们在Adapter的onBindViewHolder方法中也没有关于指定本地src的代码,因此在这种情况下就造成了bitmap实参为null。于是我又想了个投机取巧的办法,把drawableToBitmap方法就被我改成了以下这样:

    private Bitmap drawableToBitmap(Drawable drawable) {
        int w = drawable.getIntrinsicWidth();
        int h = drawable.getIntrinsicHeight();
        Bitmap bitmap;
        if (w <= 0 || h <= 0) {
            bitmap = returnBitmap();
            if (bitmap == null) {
                Resources resources = MyApplication.getContext().getResources();
                Drawable mDrawable = resources.getDrawable(R.drawable.ic_face_grey600_48dp);
                w = mDrawable.getIntrinsicWidth();
                h = mDrawable.getIntrinsicHeight();
                bitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
            }
        } else {
            bitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
        }
        Canvas canvas = new Canvas(bitmap);
        drawable.draw(canvas);
        return bitmap;
    }
我们在之前的if判断下又加了一个判断,如果returnBitmap返回的结果为null,则利用一张本地图片构建Drawable对象,然后在通过它的固定宽高来赋值给w和h,从而构建Bitmap对象。这样在任何情况下,程序都不会崩溃了。问题看起来是解决了。

解决了以上我踩的坑,我们就能利用Volley的NetworkImageView以及自定义的CircleImageView来实现一个圆形的网络图片请求控件。

但是说实话,这个控件有很多缺点,因为无论NetworkImageView还是CircleImageView在设计之初都是没有为配合对方考虑的,所以强行让CircleImageView继承NetworkImageView,在用的时候才会出现这么多坑,虽然我们把这些坑一个一个解决了,但是不得不承认有些解决方法还用上了例如本地的一些其他本来毫不相干的图片作为解决的跳板,这样做其实是很不好的,如果想让这个控件更完美,我们应该彻底分析NetworkImageView和CircleImageView两个类的源码,然后把他们以更优雅和聪明的方式结合在一起,例如在不指定本地src的时候控件也能正常运行,就像单独使用NetworkImageView的时候那样,并且最好不要用本地图片当一个中间跳板。如果大家有兴趣也许可以试一试,不过为了直接方便,我还是推荐大家用Glide和Fresco,这两个库的设计比Volley的图片加载要高明的多,功能也强劲不少。

点击复制链接 与好友分享!回本站首页
上一篇:微信小程序实战之百思不得姐精简版
下一篇:HashMap解析
相关文章
图文推荐
点击排行

关于我们 | 联系我们 | 广告服务 | 投资合作 | 版权申明 | 在线帮助 | 网站地图 | 作品发布 | Vip技术培训 | 举报中心

版权所有: 红黑联盟--致力于做实用的IT技术学习网站