Android开发之自定义Spinner的例子

 更新时间:2016年9月20日 19:55  点击:1868
本文章给各位介绍是一篇Android开发之自定义Spinner的例子,因为安卓自带的spinner不适合设计要求了,所以需要自己做一个,下面看例子。


最近在做的项目中有很多下拉框,为了实现方便就用了Android 自带的Spinner,但是自带的Spinner的样式又不符合要求,就学习了一下自定义Spinner。下面是整个步骤:

1.准备好图片

2.style中定义

<!-- spinner -->
<style name="spinner_style">
 <item name="android:background">@drawable/spinner</item>
<item name="android:paddingLeft">5dip</item>
 
3.调用

<Spinner
android:id="@+id/field_item_spinner_content"
style="@style/spinner_style"
android:layout_width="fill_parent"
android:layout_height="wrap_content"/>


4.在layout中定义simple_spinner_item.xml

<?xml version="1.0" encoding="utf-8"?>
<CheckedTextView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@android:id/text1"
android:paddingLeft="5dip"
android:paddingRight="5dip"
android:gravity="center_vertical"
android:textColor="#808080"
android:singleLine="true"
android:layout_width="fill_parent"
android:layout_height="wrap_content" />

5.java代码

ArrayAdapter<String> adapter = new ArrayAdapter<String>(mContext,R.layout.simple_spinner_item);
String level[] = getResources().getStringArray(R.array.affair_level);//资源文件
for (int i = 0; i < level.length; i++) {
    adapter.add(level[i]);
      }
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
spinner.setAdapter(adapter);

效果图:

 

Screenshot_2015-08-20-07-17-28
Fragment的使用我们可以分为:使用支持库,创建一个Fragment,创建一个动态UI,多个Fragment之间的通信。文章补充了关于FragmentManager findFragmentById 返回nul如何解决。

Android Fragment的使用


1、使用支持库

如果您的应用需要运行在3.0及以上的版本,可以忽略这部分内容。

如果您的应用使用在3.0以下、1.6及以上的版本,需要使用支持库来构建。

使用支持库的步骤:

1.使用SDK下的SDK Manager工具下载Android Support Package


2. 在您的Android工程的顶级目录下创建一个libs目录

3. 找到您的SDK下的/extras/android/support/v4/android-support-v4.jar,并且拷贝到您的项目的libs下,选中这个jar包 → 右键 → Build Path → Add to Build Path

4.在您的项目的Manifest.xml文件的标签下添加:

android:targetSdkVersion="8"/>

其中targetSdkVersion是您的软件最小支持的版本

5.如果您的项目支持3.0以下的版本,请导入如下的包:android.support.v4.*;

在使用Fragment的Activity请继承FragmentActivity而不是Activity。如果您的系统是3.0或以上版本,同样需要导入类似的包,但是可以使用普通的Activity。

2、创建一个Fragment

Fragment支持在不同的Activity中使用并且可以处理自己的输入事件以及生命周期方法等。可以看做是一个子Activity。

创建一个Fragment

创建一个Fragment和创建一个Activity很类似,继承Fragment类,重写生命周期方法,主要的不同之处就是需要重写一个onCreateView()方法来返回这个Fragment的布局。例子:


Fragment的生命周期方法依赖于Activity的生命周期,例如一个Activity的onPause()的生命周期方法被调用的时候这个Activity中的所有的Fragment的onPause()方法也将被调用。

更多的内容请参照类Fragment。

使用XML添加Fragment到Activity

尽管Fragment可以被多个Activity重用,但是您也必须把Fragment关联到一个FragmentActivity上。可以使用XML布局文件的方式来实现这种关联。

说明:上面的所说的FragmentActivity适用在API在3.0以下的版本,3.0及以上的版本可以使用普通的Activity。

例子:


上面使用fragment标签,android:name=””指定一个添加到xml中的Fragment。对于创建不同的屏幕尺寸布局的更多信息,请阅读支持不同的屏幕尺寸。


当您添加一个片段一个活动布局定义的布局XML文件中的片段,你不能删除在运行时的片段。如果您打算在用户交互和交换片段,你必须添加的活性片段的活动时第一次启动。

3、构建一个灵活的UI

FragmentManager提供了对Activity运行时的Fragment的添加、删除、替换的操作。

在Activity运行期间你可以添加Fragment而不是在XML布局文件中进行定义。如果你打算在Activity中改变Fragment的生命过程。

如果要执行添加、删除、修改的操作,你必须通过FragmentManager的对象获得一个FragmentTransaction对象,通过它的API来执行这些操作。

添加一个Fragment到一个Activity,必须把这个Fragment添加到一个容器视图中。例子:


在Activity中你可以通过getFragmentManager()来获得Fragment对象,然后通过FragmentManager对象的beginFragmentTransaction()方法来获得FragmentTransaction对象。通过它的add()方法来添加一个Fragment到当前的Activity中。

一个FragmentTransaction对象可以执行多个增删修的方法,如果你想把这些修改提交到Activity上,必须在最后调用一下这个对象的commit()方法。例子:



由于不是定义在XML布局中的,所有可以转型删除和修改的操作。

如果替换或者删除一个Fragment然后让用户可以导航到上一个Fragment,你必须在调用commit()方法之前调用addToBackStack()方法添加到回退栈。如果你把这个Fragment添加到了回退栈,在提交之后这个Fragment是会被Stop而不是Destroyed。如果用户导航到这个Fragment,这个Fragment会被Restart而不是重新创建。如果你没有把它添加到回退栈,则在删除或者替换的时候它将被Destroyed。例子:


4、与其他Fragment的交互

两个单独的Fragment之间是不应该进行通信的。应该使用他们所存在的Activity作为沟通的纽带。

为了实现两个Fragment的交互,您可以在Fragment中定义一个接口,然后再这个接口中定义一个方法,在Fragment的onAttach()方法中调用这个接口中的方法。然后让Activity实现这个方法来完成Activity和Fragment之间的通信。例子:

定义接口并调用方法:


实现接口,在这个方法中可以进行与其他Fragment的数据的交互:


可以通过FragmentManager的findFragmentById()来查找一个Fragment。




FragmentManager findFragmentById返回nul解决办法


看Fragment的两种生成方式

一.用xml标签生成

在fragment的宿主activity中添加xml标签

<fragment
        android:id="@+id/fragment_newsContent"
        android:name="com.firstcode.section4_news.NewsContentFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

name为你创建的fragment类

这种方法在activity创建时fragment已经生成了

在Activity中获取fragment实例的操作:

NewsContentFragment fragment = (NewsContentFragment) getFragmentManager().findFragmentById(R.id.fragment_newsContent);

 

二、用java代码动态生成

在fragment的宿主activity的视图文件中添加FrameLayout进行占位

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/fragmentContainer"/>

在Activity中生成fragment的操作

FragmentManager fm = getFragmentManager();
fm.beginTransaction()
    .add(R.id.fragmentContainer,“你创建的fragment类实例”)
    .commit();

 

问题分析:

    我在使用用FragmentManager.findFragmentById 返回nul的问题就在这,我是通过第二种方式来生成fragment的,也就是说在findFragmentById的实参

    我填的是FrameLayout的Id,而非fragment的Id 所以会返回null

解决方案:

      1.如果是静态生成fragment,获取fragment实例用getFragmentManager().findFragmentById
      2.如果是java代码动态生成fragment,获取fragment实例直接new 一个就好了 没必要用getFragmentManager().findFragmentById
      3.注意xml文件中的标签FrameLayout与fragment

还有个问题,我也是这样解决的 在fragment视图里给textview添加文字

Caused by: java.lang.NullPointerException: Attempt to invoke virtual method 'void android.widget.TextView.setText(java.lang.CharSequence)' on a null object reference

原因是通过java代码生成的fragment add里的Id参数填的是fragment的id 所以fragment的视图没有生成

理解两种fragment生成方式最好的文档莫过于google官方的Android Training下面是中文翻译

使用xml标签添加fragment
使用java代码动态添加fragment

android应用开发中,加载显示大图片,我们可以使用BitmapFactory.Options,本文我们来讲讲BitmapFactory.Options的使用及BitmapFactory.Options避免 内存溢出 OutOfMemoryError的优化方法。

android中BitmapFactory.Options的使用是在加载图片时,就从图片的加载和使用说起

怎样获取图片的大小?
首先我们把这个图片转成Bitmap,然后再利用Bitmap的getWidth()和getHeight()方法就可以取到图片的宽高了。
新问题又来了,在通过BitmapFactory.decodeFile(String path)方法将突破转成Bitmap时,遇到大一些的图片,我们经常会遇到OOM(Out Of Memory)的问题。怎么避免它呢?
这就用到了我们上面提到的BitmapFactory.Options这个类。
BitmapFactory.Options这个类,有一个字段叫做 inJustDecodeBounds 。SDK中对这个成员的说明是这样的:
If set to true, the decoder will return null (no bitmap), but the out…
也就是说,如果我们把它设为true,那么BitmapFactory.decodeFile(String path, Options opt)并不会真的返回一个Bitmap给你,它仅仅会把它的宽,高取回来给你,这样就不会占用太多的内存,也就不会那么频繁的发生OOM了。
示例代码如下:


BitmapFactory.Options options = new BitmapFactory.Options();

options.inJustDecodeBounds = true;

Bitmap bmp = BitmapFactory.decodeFile(path, options);/* 这里返回的bmp是null */

BitmapFactory.Options options = new BitmapFactory.Options();


options.inJustDecodeBounds = true;

Bitmap bmp = BitmapFactory.decodeFile(path, options);/* 这里返回的bmp是null */

这段代码之后,options.outWidth 和 options.outHeight就是我们想要的宽和高了。
有了宽,高的信息,我们怎样在图片不变形的情况下获取到图片指定大小的缩略图呢?
比如我们需要在图片不变形的前提下得到宽度为200的缩略图。
那么我们需要先计算一下缩放之后,图片的高度是多少 ,代码如下


int height = options.outHeight * 200 / options.outWidth;

options.outWidth = 200;

options.outHeight = height;

options.inJustDecodeBounds = false;

Bitmap bmp = BitmapFactory.decodeFile(path, options);

image.setImageBitmap(bmp);

int height = options.outHeight * 200 / options.outWidth;

options.outWidth = 200;
options.outHeight = height;
options.inJustDecodeBounds = false;
Bitmap bmp = BitmapFactory.decodeFile(path, options);
image.setImageBitmap(bmp);


这样虽然我们可以得到我们期望大小的ImageView
但是在执行BitmapFactory.decodeFile(path, options);时,并没有节约内存。要想节约内存,还需要用到BitmapFactory.Options这个类里的 inSampleSize 这个成员变量。
我们可以根据图片实际的宽高和我们期望的宽高来计算得到这个值。

options.inSampleSize = options.outWidth / 200;

/*图片长宽方向缩小倍数*

options.inSampleSize = options.outWidth / 200;

/*图片长宽方向缩小倍数*/

另外,为了节约内存我们还可以使用下面的几个字段:

options.inDither=false;/*不进行图片抖动处理*/
options.inPreferredConfig=null; /*设置让解码器以最佳方式解码*/
/* 下面两个字段需要组合使用 */
options.inPurgeable = true;
options.inInputShareable = true;



android的BitmapFactory.Options避免内存溢出OOM的优化方法

尽量不要使用setImageBitmap或setImageResource或BitmapFactory.decodeResource来设置一张大图,
因为这些函数在完成decode后,最终都是通过java层的createBitmap来完成的,需要消耗更多内存。

因此,改用先通过BitmapFactory.decodeStream方法,创建出一个bitmap,再将其设为ImageView的 source,
decodeStream最大的秘密在于其直接调用JNI>>nativeDecodeAsset()来完成decode,
无需再使用java层的createBitmap,从而节省了java层的空间。
如果在读取时加上图片的Config参数,可以跟有效减少加载的内存,从而跟有效阻止抛out of Memory异常
另外,decodeStream直接拿的图片来读取字节码了, 不会根据机器的各种分辨率来自动适应,
使用了decodeStream之后,需要在hdpi和mdpi,ldpi中配置相应的图片资源,
否则在不同分辨率机器上都是同样大小(像素点数量),显示出来的大小就不对了。

另外,以下方式也大有帮助:
1. InputStream is = this.getResources().openRawResource(R.drawable.pic1);
     BitmapFactory.Options options=new BitmapFactory.Options();
     options.inJustDecodeBounds = false;
     options.inSampleSize = 10;   //width,hight设为原来的十分一
     Bitmap btp =BitmapFactory.decodeStream(is,null,options);
2. if(!bmp.isRecycle() ){
         bmp.recycle()   //回收图片所占的内存
         system.gc()  //提醒系统及时回收
}

以下奉上一个方法:

Java代码

/**
 * 以最省内存的方式读取本地资源的图片
 * @param context
 * @param resId
 * @return
 */  
 public static Bitmap readBitMap(Context context, int resId){  
     BitmapFactory.Options opt = new BitmapFactory.Options();  
     opt.inPreferredConfig = Bitmap.Config.RGB_565;   
     opt.inPurgeable = true;  
     opt.inInputShareable = true;  
     //获取资源图片  
     InputStream is = context.getResources().openRawResource(resId);  
     return BitmapFactory.decodeStream(is,null,opt);  
 }


 

优化Dalvik虚拟机的堆内存分配

对 于Android平台来说,其托管层使用的Dalvik JavaVM从目前的表现来看还有很多地方可以优化处理,比如我们在开发一些大型游戏或耗资源的应用中可能考虑手动干涉GC处理,使用 dalvik.system.VMRuntime类提供的setTargetHeapUtilization方法可以增强程序堆内存的处理效率。当然具体 原理我们可以参考开源工程,这里我们仅说下使用方法:   private final static floatTARGET_HEAP_UTILIZATION = 0.75f; 在程序onCreate时就可以调用 VMRuntime.getRuntime().setTargetHeapUtilization(TARGET_HEAP_UTILIZATION); 即可。

介绍一下图片占用进程的内存算法吧。
android中处理图片的基础类是Bitmap,顾名思义,就是位图。占用内存的算法如下:
图片的width*height*Config。
如果Config设置为ARGB_8888,那么上面的Config就是4。一张480*320的图片占用的内存就是480*320*4 byte。
前面有人说了一下8M的概念,其实是在默认情况下android进程的内存占用量为16M,因为Bitmap他除了java中持有数据外,底层C++的 skia图形库还会持有一个SKBitmap对象,因此一般图片占用内存推荐大小应该不超过8M。这个可以调整,编译源代码时可以设置参数。

android应用开发中,如何卸载动态加载的库?本文我们提供了使用System.loadLibrary卸载及Android Jni来实现。

android 加载库后,如果重复加载同一个库,会出现已经加载得警告,也就是说,就不会重新加载so文件。这时候需要kill掉对应得activity,然后重新启动activity就可以使得so重新加载,对应代码:

int pid = android.os.Process.myPid();

android.os.Process.killProcess(pid);

Android Jni 用动态库的加载与卸载函数说明


一、当 Android 的 Virtual Machine 执行到 System.loadLibrary( "动态库名" ) 函数时,

首先会去执行 C 语言动态库里的 JNI_OnLoad 函数。
它的用途有两个:
1)告诉 Virtual Machine 当前动态库使用了哪个版本的 Jni。
  如果当前动态库中没有提供 JNI_OnLoad 函数,
  Virtual Machine 会默认为动态库使用的是最老的 Jni 1.1 版本。
  由于新版 Jni 做了许多扩充,例如 Jni 1.4 的 java.nio.ByteBuffer。
2)动态库的开发者可以在 JNI_OnLoad 函数中进行动态库内的初始化设置(Initialization),
  将此动态库中提供的各个本地函数(Native Function)登记到 Virtual Machine 里,
  以便能加快以后调用动态库中的本地函数的效率,就是初始化设置的重要一项。
  应用层级的 Java 类通过 Virtual Machine 才能调用到动态库中的本地函数。
  如果没有注册登记过的话,Virtual Machine 就在 动态库名.so 里寻找要调用的本地函数。
  如果需要连续调用很多次且每次都需要寻找一遍的话,会多花许多时间。
  因此 C 语言动态库开发者可以自已将动态库中的本地函数向 Virtual Machine 进行注册登记。
代码示例:(注:由于新浪博客不支持 C 注释,所以请将 /* */ 想像替换为 /星 星/)
jint
JNI_OnLoad( JavaVM* vm,
            void*   reserved )
{
    jint    jintResult = -1;
    JNIEnv* env = NULL;
    /*Reference types, in C.
       typedef void*   jobject;
       typedef jobject jclass; */
    jclass  cls = NULL;

    /* typedef struct {
           const char* name;      /* Java 代码中调用的函数名字 */
           const char* signature; /* 描述了函数的 参数 和 返回值 */
           void*       fnPtr;     /* 函数指针转成无符号指针 */
       } JNINativeMethod;
       其中比较复杂的是第二个参数,
       例如 "()V" 或 "(II)V" 或 "(Ljava/lang/String;)V"
       实际上这些字符是与函数的 参数 及 返回值 类型是一一对应的,
       括号()中的字符表示参数,括号后面的则代表返回值,
       例如 "()V" 就表示 void 函数名();
          "(II)V" 就表示 void 函数名( int, int );
          "(Ljava/lang/String;)V" 就表示 void 函数名( jstring );
       具体的每一个字符所表示的意义下面部分有所详见 */
    /* 动态库中的本地函数信息数组 */
    JNINativeMethod aJNINativeMethod[] = {
        { "MeasureDistance",
          "(Ljava/lang/String;)V",
          (void*)Java_MyJni_MyNDK_MyDemo_MyJniNDKDemo_MeasureDistance }
    };
     /* #if defined(__cplusplus)
           typedef _JNIEnv JNIEnv;
           typedef _JavaVM JavaVM;
       #else
           typedef const struct JNINativeInterface* JNIEnv;
           typedef const struct JNIInvokeInterface* JavaVM;
       #endif */
    /* JavaVM::GetEnv 原型为 jint (*GetEnv)(JavaVM*, void**, jint); */
    /* GetEnv()函数返回的 Jni 环境对每个线程来说是不同的,*/
    /* 因此我们必须在每次进入函数时都要重新获取 */
    if ( JNI_OK != (*vm)->GetEnv( vm,
                                  (void**)env,
                                  JNI_VERSION_1_6 ) )
    {
        /* 输出的 log 一般是到 /dev/log/ 下的三个设备中,可以用 logcat 工具查看 */
        __android_log_write( ANDROID_LOG_INFO,               /* 日志信息 */
                             "MyJniDemo",                    /* 日志标签 */
                             "Call JavaVM::GetEnv failed" ); /* 日志内容 */
       return jintResult; /* 此时返回的是负壹(-1) */
    }
    /* 如果 将动态库中的本地函数向 Virtual Machine 进行注册登记失败 的话,则 */
    /* 由于 aJNINativeMethod 是一组 函数名称 与 函数指针 的对照表,
       在程序执行期间可以多次调用registerNativeMethods函数来更换注册登记本地函数 */
    /* #if defined(__cplusplus)
           typedef _JNIEnv JNIEnv;
           typedef _JavaVM JavaVM;
       #else
           typedef const struct JNINativeInterface* JNIEnv;
           typedef const struct JNIInvokeInterface* JavaVM;
       #endif */
    /* Reference types, in C.
       typedef void*   jobject;
       typedef jobject jclass; */
    /* struct JNINativeInterface 里的函数指针
       jint (*RegisterNatives)( JNIEnv*,
                                jclass,
                                const JNINativeMethod*,
                                jint ); */
    cls = (*env)->FindClass( env,
                             /* 下面的字符串就是描述 Java 代码中的主类 */
                             "MyJni/MyNDK/MyDemo/MyJniNDKDemo" );
    if ( 0 > (*env)->RegisterNatives( env,
                                      cls,
                                      aJNINativeMethod,
                                      sizeof( aJNINativeMethod ) /
                                      sizeof( aJNINativeMethod[0] ) ) )
    {
        /* 输出的 log 一般是到 /dev/log/ 下的三个设备中,可以用 logcat 工具查看 */
        __android_log_write( ANDROID_LOG_INFO,                   /*日志信息*/
                             "MyJniDemo",                        /*日志标签*/
                             "Register native methods failed" ); /*日志内容*/
       return jintResult; /* 此时返回的是负壹(-1) */
    }
   
    jintResult = JNI_VERSION_1_6;
    /* 此函数回传 JNI_VERSION_1_6 宏值给 Virtual Machine,
       于是 Virtual Machine 就知道当前动态库所使用的 Jni 版本了 */
    return jintResult; /* JNI_VERSION_1_6(0x00010006) */
}
 
二、JNI_OnUnload 函数与 JNI_OnLoad 函数相对应。

在 JNI_OnLoad 函数中进行的动态库内的初期化设置,
要在 Virtual Machine 释放该动态库时调用 JNI_OnUnload 函数来进行善后清除。
同 Virtual Machine 调用 JNI_OnLoad 一样,
调用 JNI_Unload 函数时,也会将 JavaVM 的指针做为第一个参数传递,原型如下:
jint
JNI_OnUnload( JavaVM* vm,
              void*   reserved );
              
三、JNINativeMethod::signature 描述字符串字符意义说明:

1)基本类型对应关系:

标识符  Jni 类型       C 类型
  V    void           void
  Z    jboolean       boolean
  I    jint           int
  J    jlong          long
  D    jdouble        double
  F    jfloat         float
  B    jbyte          byte
  C    jchar          char
  S    jshort         short
 
2)基本类型数组:(则以 [ 开始,用两个字符表示)

标识串  Jni 类型        C 类型
  [Z   jbooleanArray  boolean[]
  [I   jintArray      int[]
  [J   jlongArray     long[]
  [D   jdoubleArray   double[]
  [F   jfloatArray    float[]
  [B   jbyteArray     byte[]
  [C   jcharArray     char[]
  [S   jshortArray    short[]
 
3)类(class):(则以 L 开头,以 ; 结尾,中间是用 / 隔开的 包 及 类名)

标识串        Java 类型  Jni 类型
L包1/包n/类名;     类名     jobject
例子:
Ljava/net/Socket; Socket      jobject

4)例外(String 类):

标识串               Java 类型  Jni 类型
Ljava/lang/String;  String    jstring

5)嵌套类(类位于另一个类之中,则用$作为类名间的分隔符)

标识串                         Java 类型  Jni 类型
L包1/包n/类名$嵌套类名;              类名      jobject
例子:
Landroid/os/FileUtils$FileStatus;  FileStatus  jobject

Android ListView异步加载图片乱序问题如果对于要求不高的朋友来讲这个不算是问题当然对于一定要按我们后台上传的方式来排序图片那肯定就是一个大问题了,具体看要求来判定是否是有问题了。

在Android所有系统自带的控件当中,ListView这个控件算是用法比较复杂的了,关键是用法复杂也就算了,它还经常会出现一些稀奇古怪的问题,让人非常头疼。比如说在ListView中加载图片,如果是同步加载图片倒还好,但是一旦使用异步加载图片那么问题就来了,这个问题我相信很多Android开发者都曾经遇到过,就是异步加载图片会出现错位乱序的情况。遇到这个问题时,不少人在网上搜索找到了相应的解决方案,但是真正深入理解这个问题出现的原因并对症解决的人恐怕还并不是很多。那么今天我们就来具体深入分析一下ListView异步加载图片出现乱序问题的原因,以及怎么样对症下药去解决它。

本篇文章的原理基础建立在上一篇文章之上,如果你对ListView的工作原理还不够了解的话,建议先去阅读 Android ListView工作原理完全解析,带你从源码的角度彻底理解 。

问题重现
要想解决问题首先我们要把问题重现出来,这里只需要搭建一个最基本的ListView项目,然后在ListView中去异步请求图片并显示,问题就能够得以重现了,那么我们就新建一个ListViewTest项目。

项目建好之后第一个要解决的是数据源的问题,由于ListView中需要从网络上请求图片,那么我就提前准备好了许多张图片,将它们上传到了我的CSDN相册当中,然后新建一个Images类,将所有相册中图片的URL地址都配置进去就可以了,代码如下所示:


public class Images {

    public final static String[] imageUrls = new String[] {
     "201508/05/1438760758_3497.jpg", 
        "201508/05/1438760758_6667.jpg",
        "201508/05/1438760757_3588.jpg",
        "201508/05/1438760756_3304.jpg",
        "201508/05/1438760755_6715.jpeg",
        "201508/05/1438760726_5120.jpg",
        "201508/05/1438760726_8364.jpg",
        "201508/05/1438760725_4031.jpg",
        "201508/05/1438760724_9463.jpg",
        "201508/05/1438760724_2371.jpg",
        "201508/05/1438760707_4653.jpg",
        "201508/05/1438760706_6864.jpg",
        "201508/05/1438760706_9279.jpg",
        "201508/05/1438760704_2341.jpg",
        "201508/05/1438760704_5707.jpg",
        "201508/05/1438760685_5091.jpg",
        "201508/05/1438760685_4444.jpg",
        "201508/05/1438760684_8827.jpg",
        "201508/05/1438760683_3691.jpg",
        "201508/05/1438760683_7315.jpg",
        "201508/05/1438760663_7318.jpg",
        "201508/05/1438760662_3454.jpg",
        "201508/05/1438760662_5113.jpg",
        "201508/05/1438760661_3305.jpg",
        "201508/05/1438760661_7416.jpg",
        "201508/05/1438760589_2946.jpg",
        "201508/05/1438760589_1100.jpg",
        "201508/05/1438760588_8297.jpg",
        "201508/05/1438760587_2575.jpg",
        "201508/05/1438760587_8906.jpg",
        "201508/05/1438760550_2875.jpg",
        "201508/05/1438760550_9517.jpg",
        "201508/05/1438760549_7093.jpg",
        "201508/05/1438760549_1352.jpg",
        "201508/05/1438760548_2780.jpg",
        "201508/05/1438760531_1776.jpg",
        "201508/05/1438760531_1380.jpg",
        "201508/05/1438760530_4944.jpg",
        "201508/05/1438760530_5750.jpg",
        "201508/05/1438760529_3289.jpg",
        "201508/05/1438760500_7871.jpg",
        "201508/05/1438760500_6063.jpg",
        "201508/05/1438760499_6304.jpeg",
        "201508/05/1438760499_5081.jpg",
        "201508/05/1438760498_7007.jpg",
        "201508/05/1438760478_3128.jpg",
        "201508/05/1438760478_6766.jpg",
        "201508/05/1438760477_1358.jpg",
        "201508/05/1438760477_3540.jpg",
        "201508/05/1438760476_1240.jpg",
        "201508/05/1438760446_7993.jpg",
        "201508/05/1438760446_3641.jpg",
        "201508/05/1438760445_3283.jpg",
        "201508/05/1438760444_8623.jpg",
        "201508/05/1438760444_6822.jpg",
        "201508/05/1438760422_2224.jpg",
        "201508/05/1438760421_2824.jpg",
        "201508/05/1438760420_2660.jpg",
        "201508/05/1438760420_7188.jpg",
        "201508/05/1438760419_4123.jpg",
    };
}设置好了图片源之后,我们需要一个ListView来展示所有的图片。打开或修改activity_main.xml中的代码,如下所示:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <ListView
        android:id="@+id/list_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        >
    </ListView>

</LinearLayout>很简单,只是在LinearLayout中写了一个ListView而已。接着我们要定义ListView中每一个子View的布局,新建一个image_item.xml布局,加入如下代码:


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

    <ImageView
        android:id="@+id/image"
        android:layout_width="match_parent"
        android:layout_height="120dp"
        android:src="@drawable/empty_photo"
        android:scaleType="fitXY"/>

</LinearLayout>仍然很简单,image_item.xml布局中只有一个ImageView控件,就是用它来显示图片的,控件在默认情况下会显示一张empty_photo。这样我们就把所有的布局文件都写好了。

接下来新建ImageAdapter做为ListView的适配器,代码如下所示:

 
public class ImageAdapter extends ArrayAdapter<String> {

 /**
  * 图片缓存技术的核心类,用于缓存所有下载好的图片,在程序内存达到设定值时会将最少最近使用的图片移除掉。
  */
 private LruCache<String, BitmapDrawable> mMemoryCache;

 public ImageAdapter(Context context, int resource, String[] objects) {
  super(context, resource, objects);
  // 获取应用程序最大可用内存
  int maxMemory = (int) Runtime.getRuntime().maxMemory();
  int cacheSize = maxMemory / 8;
  mMemoryCache = new LruCache<String, BitmapDrawable>(cacheSize) {
   @Override
   protected int sizeOf(String key, BitmapDrawable drawable) {
    return drawable.getBitmap().getByteCount();
   }
  };
 }

 @Override
 public View getView(int position, View convertView, ViewGroup parent) {
  String url = getItem(position);
  View view;
  if (convertView == null) {
   view = LayoutInflater.from(getContext()).inflate(R.layout.image_item, null);
  } else {
   view = convertView;
  }
  ImageView image = (ImageView) view.findViewById(R.id.image);
  BitmapDrawable drawable = getBitmapFromMemoryCache(url);
  if (drawable != null) {
   image.setImageDrawable(drawable);
  } else {
   BitmapWorkerTask task = new BitmapWorkerTask(image);
   task.execute(url);
  }
  return view;
 }

 /**
  * 将一张图片存储到LruCache中。
  *
  * @param key
  *            LruCache的键,这里传入图片的URL地址。
  * @param drawable
  *            LruCache的键,这里传入从网络上下载的BitmapDrawable对象。
  */
 public void addBitmapToMemoryCache(String key, BitmapDrawable drawable) {
  if (getBitmapFromMemoryCache(key) == null) {
   mMemoryCache.put(key, drawable);
  }
 }

 /**
  * 从LruCache中获取一张图片,如果不存在就返回null。
  *
  * @param key
  *            LruCache的键,这里传入图片的URL地址。
  * @return 对应传入键的BitmapDrawable对象,或者null。
  */
 public BitmapDrawable getBitmapFromMemoryCache(String key) {
  return mMemoryCache.get(key);
 }

 /**
  * 异步下载图片的任务。
  *
  * @author guolin
  */
 class BitmapWorkerTask extends AsyncTask<String, Void, BitmapDrawable> {

  private ImageView mImageView;

  public BitmapWorkerTask(ImageView imageView) {
   mImageView = imageView;
  }

  @Override
  protected BitmapDrawable doInBackground(String... params) {
   String imageUrl = params[0];
   // 在后台开始下载图片
   Bitmap bitmap = downloadBitmap(imageUrl);
   BitmapDrawable drawable = new BitmapDrawable(getContext().getResources(), bitmap);
   addBitmapToMemoryCache(imageUrl, drawable);
   return drawable;
  }

  @Override
  protected void onPostExecute(BitmapDrawable drawable) {
   if (mImageView != null && drawable != null) {
    mImageView.setImageDrawable(drawable);
   }
  }

  /**
   * 建立HTTP请求,并获取Bitmap对象。
   *
   * @param imageUrl
   *            图片的URL地址
   * @return 解析后的Bitmap对象
   */
  private Bitmap downloadBitmap(String imageUrl) {
   Bitmap bitmap = null;
   HttpURLConnection con = null;
   try {
    URL url = new URL(imageUrl);
    con = (HttpURLConnection) url.openConnection();
    con.setConnectTimeout(5 * 1000);
    con.setReadTimeout(10 * 1000);
    bitmap = BitmapFactory.decodeStream(con.getInputStream());
   } catch (Exception e) {
    e.printStackTrace();
   } finally {
    if (con != null) {
     con.disconnect();
    }
   }
   return bitmap;
  }

 }

}

ImageAdapter中的代码还算是比较简单的,在getView()方法中首先根据当前的位置获取到图片的URL地址,然后使用inflate()方法加载image_item.xml这个布局,并获取到ImageView控件的实例,接下来开启了一个BitmapWorkerTask异步任务来从网络上加载图片,最终将加载好的图片设置到ImageView上面。注意这里为了防止图片占用过多的内存,我们还是使用了LruCache技术来进行内存控制,对这个技术不熟悉的朋友可以参考我之前的一篇文章 Android高效加载大图、多图解决方案,有效避免程序OOM 。

最后,程序主界面的代码就非常简单了,修改MainActivity中的代码,如下所示:

 
public class MainActivity extends Activity {
 
 private ListView listView;
 
 @Override
 protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_main);
  listView = (ListView) findViewById(R.id.list_view);
  ImageAdapter adapter = new ImageAdapter(this, 0, Images.imageThumbUrls);
  listView.setAdapter(adapter);
 }


}这就是整个程序所有的代码了,记得还需要在AndroidManifest.xml中添加INTERNET权限。

那么目前程序的思路其实是很简单的,我们在ListView的getView()方法中开启异步请求,从网络上获取图片,当图片获取成功就后就将图片显示到ImageView上面。看起来没什么问题对吗?那么现在我们就来运行一下程序看一看效果吧。

 


恩?怎么会这个样子,当滑动ListView的时候,图片竟然会自动变来变去,而且图片显示的位置也不正确,简直快乱成一锅粥了!可是我们所有的逻辑都很简单呀,怎么会导致出现这种图片自动变来变去的情况?很遗憾,这是由于Listview内部的工作机制所导致的,如果你对Listview的工作机制不了解,那么就会很难理解这种现象,不过好在上篇文章中我已经讲解过ListView的工作原理了,因此下面就让我们一起分析一下这个问题出现的原因。

原因分析
上篇文章中已经提到了,ListView之所以能够实现加载成百上千条数据都不会OOM,最主要在于它内部优秀的实现机制。虽然作为普通的使用者,我们大可不必关心ListView内部到底是怎么实现的,但是当你了解了它的内部原理之后,很多之前难以解释的问题都变得有理有据了。

ListView在借助RecycleBin机制的帮助下,实现了一个生产者和消费者的模式,不管有任意多条数据需要显示,ListView中的子View其实来来回回就那么几个,移出屏幕的子View会很快被移入屏幕的数据重新利用起来,原理示意图如下所示:

 


那么这里我们就可以思考一下了,目前数据源当中大概有60个图片的URL地址,而根据ListView的工作原理,显然不可能为每张图片都单独分配一个ImageView控件,ImageView控件的个数其实就比一屏能显示的图片数量稍微多一点而已,移出屏幕的ImageView控件会进入到RecycleBin当中,而新进入屏幕的元素则会从RecycleBin中获取ImageView控件。

那么,每当有新的元素进入界面时就会回调getView()方法,而在getView()方法中会开启异步请求从网络上获取图片,注意网络操作都是比较耗时的,也就是说当我们快速滑动ListView的时候就很有可能出现这样一种情况,某一个位置上的元素进入屏幕后开始从网络上请求图片,但是还没等图片下载完成,它就又被移出了屏幕。这种情况下会产生什么样的现象呢?根据ListView的工作原理,被移出屏幕的控件将会很快被新进入屏幕的元素重新利用起来,而如果在这个时候刚好前面发起的图片请求有了响应,就会将刚才位置上的图片显示到当前位置上,因为虽然它们位置不同,但都是共用的同一个ImageView实例,这样就出现了图片乱序的情况。

但是还没完,新进入屏幕的元素它也会发起一条网络请求来获取当前位置的图片,等到图片下载完的时候会设置到同样的ImageView上面,因此就会出现先显示一张图片,然后又变成了另外一张图片的情况,那么刚才我们看到的图片会自动变来变去的情况也就得到了解释。

问题原因已经分析出来了,但是这个问题该怎么解决呢?说实话,ListView异步加载图片的问题并没有什么标准的解决方案,很多人都有自己的一套解决思路,这里我准备给大家讲解三种比较经典的解决办法,大家通过任何一种都可以解决这个问题,但是我们每多学习一种思路,水平就能够更进一步的提高。

解决方案一  使用findViewWithTag
findViewWithTag算是一种比较简单易懂的解决方案,其实早在 Android照片墙应用实现,再多的图片也不怕崩溃 这篇文章当中,我就采用了findViewWithTag来避免图片出现乱序的情况。那么这里我们先来看看怎么通过修改代码把这个问题解决掉,然后再研究一下findViewWithTag的工作原理。

使用findViewWithTag并不需要修改太多的代码,只需要改动ImageAdapter这一个类就可以了,如下所示:

public class ImageAdapter extends ArrayAdapter<String> {
 
 private ListView mListView;

 ......

 @Override
 public View getView(int position, View convertView, ViewGroup parent) {
  if (mListView == null) { 
            mListView = (ListView) parent; 
        }
  String url = getItem(position);
  View view;
  if (convertView == null) {
   view = LayoutInflater.from(getContext()).inflate(R.layout.image_item, null);
  } else {
   view = convertView;
  }
  ImageView image = (ImageView) view.findViewById(R.id.image);
  image.setImageResource(R.drawable.empty_photo);
        image.setTag(url);
  BitmapDrawable drawable = getBitmapFromMemoryCache(url);
  if (drawable != null) {
   image.setImageDrawable(drawable);
  } else {
   BitmapWorkerTask task = new BitmapWorkerTask();
   task.execute(url);
  }
  return view;
 }

 ......

 /**
  * 异步下载图片的任务。
  *
  * @author guolin
  */
 class BitmapWorkerTask extends AsyncTask<String, Void, BitmapDrawable> {

  String imageUrl;

  @Override
  protected BitmapDrawable doInBackground(String... params) {
   imageUrl = params[0];
   // 在后台开始下载图片
   Bitmap bitmap = downloadBitmap(imageUrl);
   BitmapDrawable drawable = new BitmapDrawable(getContext().getResources(), bitmap);
   addBitmapToMemoryCache(imageUrl, drawable);
   return drawable;
  }

  @Override
  protected void onPostExecute(BitmapDrawable drawable) {
   ImageView imageView = (ImageView) mListView.findViewWithTag(imageUrl); 
            if (imageView != null && drawable != null) { 
                imageView.setImageDrawable(drawable); 
            }
  }

  ......

 }

}
改动的地方就只有这么多,那么我们来分析一下。由于使用findViewWithTag必须要有ListView的实例才行,那么我们在Adapter中怎样才能拿到ListView的实例呢?其实如果你仔细通读了上一篇文章就能知道,getView()方法中传入的第三个参数其实就是ListView的实例,那么这里我们定义一个全局变量mListView,然后在getView()方法中判断它是否为空,如果为空就把parent这个参数赋值给它。

另外在getView()方法中我们还做了一个操作,就是调用了ImageView的setTag()方法,并把当前位置图片的URL地址作为参数传了进去,这个是为后续的findViewWithTag()方法做准备。

最后,我们修改了BitmapWorkerTask的构造函数,这里不再通过构造函数把ImageView的实例传进去了,而是在onPostExecute()方法当中通过ListView的findVIewWithTag()方法来去获取ImageView控件的实例。获取到控件实例后判断下是否为空,如果不为空就让图片显示到控件上。

这里我们可以尝试分析一下findViewWithTag的工作原理,其实顾名思义,这个方法就是通过Tag的名字来获取具备该Tag名的控件,我们先要调用控件的setTag()方法来给控件设置一个Tag,然后再调用ListView的findViewWithTag()方法使用相同的Tag名来找回控件。

那么为什么用了findViewWithTag()方法之后,图片就不会再出现乱序情况了呢?其实原因很简单,由于ListView中的ImageView控件都是重用的,移出屏幕的控件很快会被进入屏幕的图片重新利用起来,那么getView()方法就会再次得到执行,而在getView()方法中会为这个ImageView控件设置新的Tag,这样老的Tag就会被覆盖掉,于是这时再调用findVIewWithTag()方法并传入老的Tag,就只能得到null了,而我们判断只有ImageView不等于null的时候才会设置图片,这样图片乱序的问题也就不存在了。

这是第一种解决方案。

解决方案二  使用弱引用关联
虽然这里我给这种解决方案起名叫弱引用关联,但实际上弱引用只是辅助手段而已,最主要的还是关联,这种解决方案的本质是要让ImageView和BitmapWorkerTask之间建立一个双向关联,互相持有对方的引用,再通过适当的逻辑判断来解决图片乱序问题,然后为了防止出现内存泄漏的情况,双向关联要使用弱引用的方式建立。相比于第一种解决方案,第二种解决方案要明显复杂不少,但在性能和效率方面都会有更好的表现。

我们仍然只需要改动ImageAdapter中的代码,但这次改动的地方比较多,所以我就把ImageAdapter中的全部代码都贴出来了,如下所示:


public class ImageAdapter extends ArrayAdapter<String> {
 
 private ListView mListView;
 
 private Bitmap mLoadingBitmap;

 /**
  * 图片缓存技术的核心类,用于缓存所有下载好的图片,在程序内存达到设定值时会将最少最近使用的图片移除掉。
  */
 private LruCache<String, BitmapDrawable> mMemoryCache;

 public ImageAdapter(Context context, int resource, String[] objects) {
  super(context, resource, objects);
  mLoadingBitmap = BitmapFactory.decodeResource(context.getResources(),
    R.drawable.empty_photo);
  // 获取应用程序最大可用内存
  int maxMemory = (int) Runtime.getRuntime().maxMemory();
  int cacheSize = maxMemory / 8;
  mMemoryCache = new LruCache<String, BitmapDrawable>(cacheSize) {
   @Override
   protected int sizeOf(String key, BitmapDrawable drawable) {
    return drawable.getBitmap().getByteCount();
   }
  };
 }

 @Override
 public View getView(int position, View convertView, ViewGroup parent) {
  if (mListView == null) { 
            mListView = (ListView) parent; 
        }
  String url = getItem(position);
  View view;
  if (convertView == null) {
   view = LayoutInflater.from(getContext()).inflate(R.layout.image_item, null);
  } else {
   view = convertView;
  }
  ImageView image = (ImageView) view.findViewById(R.id.image);
  BitmapDrawable drawable = getBitmapFromMemoryCache(url);
  if (drawable != null) {
   image.setImageDrawable(drawable);
  } else if (cancelPotentialWork(url, image)) {
   BitmapWorkerTask task = new BitmapWorkerTask(image);
   AsyncDrawable asyncDrawable = new AsyncDrawable(getContext()
     .getResources(), mLoadingBitmap, task);
   image.setImageDrawable(asyncDrawable);
   task.execute(url);
  }
  return view;
 }
 
 /**
  * 自定义的一个Drawable,让这个Drawable持有BitmapWorkerTask的弱引用。
  */
 class AsyncDrawable extends BitmapDrawable {

  private WeakReference<BitmapWorkerTask> bitmapWorkerTaskReference;

  public AsyncDrawable(Resources res, Bitmap bitmap,
    BitmapWorkerTask bitmapWorkerTask) {
   super(res, bitmap);
   bitmapWorkerTaskReference = new WeakReference<BitmapWorkerTask>(
     bitmapWorkerTask);
  }

  public BitmapWorkerTask getBitmapWorkerTask() {
   return bitmapWorkerTaskReference.get();
  }

 }
 
 /**
  * 获取传入的ImageView它所对应的BitmapWorkerTask。
  */
 private BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) {
        if (imageView != null) {
            Drawable drawable = imageView.getDrawable();
            if (drawable instanceof AsyncDrawable) {
                AsyncDrawable asyncDrawable = (AsyncDrawable) drawable;
                return asyncDrawable.getBitmapWorkerTask();
            }
        }
        return null;
    }
 
 /**
  * 取消掉后台的潜在任务,当认为当前ImageView存在着一个另外图片请求任务时
  * ,则把它取消掉并返回true,否则返回false。
  */
    public boolean cancelPotentialWork(String url, ImageView imageView) {
        BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
        if (bitmapWorkerTask != null) {
            String imageUrl = bitmapWorkerTask.imageUrl;
            if (imageUrl == null || !imageUrl.equals(url)) {
                bitmapWorkerTask.cancel(true);
            } else {
                return false;
            }
        }
        return true;
    }

 /**
  * 将一张图片存储到LruCache中。
  *
  * @param key
  *            LruCache的键,这里传入图片的URL地址。
  * @param drawable
  *            LruCache的键,这里传入从网络上下载的BitmapDrawable对象。
  */
 public void addBitmapToMemoryCache(String key, BitmapDrawable drawable) {
  if (getBitmapFromMemoryCache(key) == null) {
   mMemoryCache.put(key, drawable);
  }
 }

 /**
  * 从LruCache中获取一张图片,如果不存在就返回null。
  *
  * @param key
  *            LruCache的键,这里传入图片的URL地址。
  * @return 对应传入键的BitmapDrawable对象,或者null。
  */
 public BitmapDrawable getBitmapFromMemoryCache(String key) {
  return mMemoryCache.get(key);
 }

 /**
  * 异步下载图片的任务。
  *
  * @author guolin
  */
 class BitmapWorkerTask extends AsyncTask<String, Void, BitmapDrawable> {

  String imageUrl;
  
  private WeakReference<ImageView> imageViewReference;
  
  public BitmapWorkerTask(ImageView imageView) { 
   imageViewReference = new WeakReference<ImageView>(imageView);
        } 

  @Override
  protected BitmapDrawable doInBackground(String... params) {
   imageUrl = params[0];
   // 在后台开始下载图片
   Bitmap bitmap = downloadBitmap(imageUrl);
   BitmapDrawable drawable = new BitmapDrawable(getContext().getResources(), bitmap);
   addBitmapToMemoryCache(imageUrl, drawable);
   return drawable;
  }

  @Override
  protected void onPostExecute(BitmapDrawable drawable) {
   ImageView imageView = getAttachedImageView();
            if (imageView != null && drawable != null) { 
                imageView.setImageDrawable(drawable); 
            }
  }
  
  /**
   * 获取当前BitmapWorkerTask所关联的ImageView。
   */
  private ImageView getAttachedImageView() {
            ImageView imageView = imageViewReference.get();
            BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
            if (this == bitmapWorkerTask) {
                return imageView;
            }
            return null;
        }

  /**
   * 建立HTTP请求,并获取Bitmap对象。
   *
   * @param imageUrl
   *            图片的URL地址
   * @return 解析后的Bitmap对象
   */
  private Bitmap downloadBitmap(String imageUrl) {
   Bitmap bitmap = null;
   HttpURLConnection con = null;
   try {
    URL url = new URL(imageUrl);
    con = (HttpURLConnection) url.openConnection();
    con.setConnectTimeout(5 * 1000);
    con.setReadTimeout(10 * 1000);
    bitmap = BitmapFactory.decodeStream(con.getInputStream());
   } catch (Exception e) {
    e.printStackTrace();
   } finally {
    if (con != null) {
     con.disconnect();
    }
   }
   return bitmap;
  }

 }

}那么我们一点点开始解析。首先刚才说到的,ImageView和BitmapWorkerTask之间要建立一个双向的弱引用关联,上述代码中已经建立好了。ImageView中可以获取到它所对应的BitmapWorkerTask,而BitmapWorkerTask也可以获取到它所对应的ImageView。

下面来看一下这个双向弱引用关联是怎么建立的。BitmapWorkerTask指向ImageView的弱引用关联比较简单,就是在BitmapWorkerTask中加入一个构造函数,并在构造函数中要求传入ImageView这个参数。不过我们不再直接持有ImageView的引用,而是使用WeakReference对ImageView进行了一层包装,这样就OK了。

但是ImageView指向BitmapWorkerTask的弱引用关联就没这么容易了,因为我们很难将BitmapWorkerTask的一个弱引用直接设置到ImageView当中。这该怎么办呢?这里使用了一个比较巧的方法,就是借助自定义Drawable的方式来实现。可以看到,我们自定义了一个AsyncDrawable类并让它继承自BitmapDrawable,然后重写了AsyncDrawable的构造函数,在构造函数中要求把BitmapWorkerTask传入,然后在这里给它包装了一层弱引用。那么现在AsyncDrawable指向BitmapWorkerTask的关联已经有了,但是ImageView指向BitmapWorkerTask的关联还不存在,怎么办呢?很简单,让ImageView和AsyncDrawable再关联一下就可以了。可以看到,在getView()方法当中,我们调用了ImageView的setImageDrawable()方法把AsyncDrawable设置了进去,那么ImageView就可以通过getDrawable()方法获取到和它关联的AsyncDrawable,然后再借助AsyncDrawable就可以获取到BitmapWorkerTask了。这样ImageView指向BitmapWorkerTask的弱引用关联也成功建立。

现在双向弱引用的关联已经建立好了,接下来就是逻辑判断的工作了。那么怎样通过逻辑判断来避免图片出现乱序的情况呢?这里我们引入了两个方法,一个是getBitmapWorkerTask()方法,这个方法可以根据传入的ImageView来获取到它对应的BitmapWorkerTask,内部的逻辑就是先获取ImageView对应的AsyncDrawable,再获取AsyncDrawable对应的BitmapWorkerTask。另一个是getAttachedImageView()方法,这个方法会获取当前BitmapWorkerTask所关联的ImageView,然后调用getBitmapWorkerTask()方法来获取该ImageView所对应的BitmapWorkerTask,最后判断,如果获取到的BitmapWorkerTask等于this,也就是当前的BitmapWorkerTask,那么就将ImageView返回,否则就返回null。最后,在onPostExecute()方法当中,只需要使用getAttachedImageView()方法获取到的ImageView来显示图片就可以了。

那么为什么做了这个逻辑判断之后,图片乱序的问题就可以得到解决呢?其实最主要的奥秘就是在getAttachedImageView()方法当中,它会使用当前BitmapWorkerTask所关联的ImageView来反向获取这个ImageView所关联的BitmapWorkerTask,然后用这两个BitmapWorkerTask做对比,如果发现是同一个BitmapWorkerTask才会返回ImageView,否则就返回null。那么什么情况下这两个BitmapWorkerTask才会不同呢?比如说某个图片被移出了屏幕,它的ImageView被另外一个新进入屏幕的图片重用了,那么就会给这个ImageView关联一个新的BitmapWorkerTask,这种情况下,上一个BitmapWorkerTask和新的BitmapWorkerTask肯定就不相等了,这时getAttachedImageView()方法会返回null,而我们又判断ImageView等于null的话是不会设置图片的,因此就不会出现图片乱序的情况了。

除此之外还有另外一个方法非常值得大家注意,就是cancelPotentialWork()方法,这个方法可以大大提高整个ListView图片加载的工作效率。这个方法接收两个参数,一个图片的url,一个ImageView。看一下它的内部逻辑,首先它也是调用了getBitmapWorkerTask()方法来获取传入的ImageView所对应的BitmapWorkerTask,接下来拿BitmapWorkerTask中的imageUrl和传入的url做比较,如果两个url不等的话就调用BitmapWorkerTask的cancel()方法,然后返回true,如果两个url相等的话就返回false。

那么这段逻辑是什么意思呢?其实并不复杂,两个url做比对时,如果发现是相同的,说明请求的是同一张图片,那么直接返回false,这样就不会再去启动BitmapWorkerTask来请求图片,而如果两个url不相同,说明这个ImageView被另外一张图片重新利用了,这个时候就调用了BitmapWorkerTask的cancel()方法把之前的请求取消掉,然后重新启动BitmapWorkerTask来去请求新图片。有了这个操作保护之后,就可以把一些已经移出屏幕的无效的图片请求过滤掉,从而整体提升ListView加载图片的工作效率。

这是第二种解决方案。


解决方案三  使用NetworkImageView
前面两种解决方案都需要我们自己去做额外的逻辑处理,因为ImageView本身是不能自动解决这个问题的,但是如果我们使用NetworkImageView这个控件的话就非常简单了,它自身就已经考虑到了这个问题,我们直接使用它就可以了,不用做任何额外的处理也不会出现图片乱序的情况。

NetworkImageView是Volley当中提供的控件,对于这个控件我之前专门写过一篇博客来讲解,还不熟悉这个控件的朋友可以先去阅读 Android Volley完全解析(二),使用Volley加载网络图片 。


下面我们看一下如何用NetworkImageView来解决这个问题,首先需要修改一下image_item.xml文件,因为我们已经不再使用ImageView控件了,代码如下所示:


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

    <com.android.volley.toolbox.NetworkImageView
        android:id="@+id/image"
        android:layout_width="match_parent"
        android:layout_height="120dp"
        android:src="@drawable/empty_photo"
        android:scaleType="fitXY"/>

</LinearLayout>很简单,只是把ImageView替换成了NetworkImageView。然后修改ImageAdapter中的代码,如下所示:

 

public class ImageAdapter extends ArrayAdapter<String> {
 
 ImageLoader mImageLoader;

 public ImageAdapter(Context context, int resource, String[] objects) {
  super(context, resource, objects);
  RequestQueue queue = Volley.newRequestQueue(context);
  mImageLoader = new ImageLoader(queue, new BitmapCache());
 }

 @Override
 public View getView(int position, View convertView, ViewGroup parent) {
  String url = getItem(position);
  View view;
  if (convertView == null) {
   view = LayoutInflater.from(getContext()).inflate(R.layout.image_item, null);
  } else {
   view = convertView;
  }
  NetworkImageView image = (NetworkImageView) view.findViewById(R.id.image);
  image.setDefaultImageResId(R.drawable.empty_photo);
  image.setErrorImageResId(R.drawable.empty_photo);
  image.setImageUrl(url, mImageLoader);
  return view;
 }

 /**
  * 使用LruCache来缓存图片
  */
 public class BitmapCache implements ImageCache {

  private LruCache<String, Bitmap> mCache;

  public BitmapCache() {
   // 获取应用程序最大可用内存
   int maxMemory = (int) Runtime.getRuntime().maxMemory();
   int cacheSize = maxMemory / 8;
   mCache = new LruCache<String, Bitmap>(cacheSize) {
    @Override
    protected int sizeOf(String key, Bitmap bitmap) {
     return bitmap.getRowBytes() * bitmap.getHeight();
    }
   };
  }

  @Override
  public Bitmap getBitmap(String url) {
   return mCache.get(url);
  }

  @Override
  public void putBitmap(String url, Bitmap bitmap) {
   mCache.put(url, bitmap);
  }

 }

}没错,就是这么简单,一共60行左右的代码搞定一切!我们不需要自己再去写一个BitmapWorkerTask来处理图片的下载和显示,也不需要自己再去管理LruCache的逻辑,一切NetworkImageView都帮我们做好了。至于上面的代码我就不再做解释了,因为实在是太简单了。

那么当然了,虽然现在没有做任何额外的逻辑处理,但是也根本不会出现图片乱序的情况,因为NetworkImageView在内部都帮我们处理掉了。不过大家可能都很好奇,NetworkImageView到底是如何做到的呢?那么就让我们来分析一下它的源码吧。

NetworkImageView中开始加载图片的代码是setImageUrl()方法,源码分析就从这里开始吧,如下所示:

/**
 * Sets URL of the image that should be loaded into this view. Note that calling this will
 * immediately either set the cached image (if available) or the default image specified by
 * {@link NetworkImageView#setDefaultImageResId(int)} on the view.
 *
 * NOTE: If applicable, {@link NetworkImageView#setDefaultImageResId(int)} and
 * {@link NetworkImageView#setErrorImageResId(int)} should be called prior to calling
 * this function.
 *
 * @param url The URL that should be loaded into this ImageView.
 * @param imageLoader ImageLoader that will be used to make the request.
 */
public void setImageUrl(String url, ImageLoader imageLoader) {
    mUrl = url;
    mImageLoader = imageLoader;
    // The URL has potentially changed. See if we need to load it.
    loadImageIfNecessary(false);
}

setImageUrl()方法中并没有几行代码,让人值得留意的是loadImageIfNecessary()这个方法,看上去具体加载图片的逻辑就是在这里进行的,那么我们就跟进去瞧一瞧:

/**
 * Loads the image for the view if it isn't already loaded.
 * @param isInLayoutPass True if this was invoked from a layout pass, false otherwise.
 */
private void loadImageIfNecessary(final boolean isInLayoutPass) {
    int width = getWidth();
    int height = getHeight();

    boolean isFullyWrapContent = getLayoutParams() != null
            && getLayoutParams().height == LayoutParams.WRAP_CONTENT
            && getLayoutParams().width == LayoutParams.WRAP_CONTENT;
    // if the view's bounds aren't known yet, and this is not a wrap-content/wrap-content
    // view, hold off on loading the image.
    if (width == 0 && height == 0 && !isFullyWrapContent) {
        return;
    }

    // if the URL to be loaded in this view is empty, cancel any old requests and clear the
    // currently loaded image.
    if (TextUtils.isEmpty(mUrl)) {
        if (mImageContainer != null) {
            mImageContainer.cancelRequest();
            mImageContainer = null;
        }
        setDefaultImageOrNull();
        return;
    }

    // if there was an old request in this view, check if it needs to be canceled.
    if (mImageContainer != null && mImageContainer.getRequestUrl() != null) {
        if (mImageContainer.getRequestUrl().equals(mUrl)) {
            // if the request is from the same URL, return.
            return;
        } else {
            // if there is a pre-existing request, cancel it if it's fetching a different URL.
            mImageContainer.cancelRequest();
            setDefaultImageOrNull();
        }
    }

    // The pre-existing content of this view didn't match the current URL. Load the new image
    // from the network.
    ImageContainer newContainer = mImageLoader.get(mUrl,
            new ImageListener() {
                @Override
                public void onErrorResponse(VolleyError error) {
                    if (mErrorImageId != 0) {
                        setImageResource(mErrorImageId);
                    }
                }

                @Override
                public void onResponse(final ImageContainer response, boolean isImmediate) {
                    // If this was an immediate response that was delivered inside of a layout
                    // pass do not set the image immediately as it will trigger a requestLayout
                    // inside of a layout. Instead, defer setting the image by posting back to
                    // the main thread.
                    if (isImmediate && isInLayoutPass) {
                        post(new Runnable() {
                            @Override
                            public void run() {
                                onResponse(response, false);
                            }
                        });
                        return;
                    }

                    if (response.getBitmap() != null) {
                        setImageBitmap(response.getBitmap());
                    } else if (mDefaultImageId != 0) {
                        setImageResource(mDefaultImageId);
                    }
                }
            });

    // update the ImageContainer to be the new bitmap container.
    mImageContainer = newContainer;

}这里在第43行调用了ImageLoader的get()方法来去请求图片,get()方法会返回一个ImageContainer对象,这个对象封装了图片请求地址、Bitmap等数据,每个NetworkImageView中都会对应一个ImageContainer。然后在第31行我们看到,这里从ImageContainer对象中获取封装的图片请求地址,并拿来和当前的请求地址做对比,如果相同的话说明这是一条重复的请求,就直接return掉,如果不同的话就调用cancelRequest()方法将请求取消掉,然后将图片设置为默认图片并重新发起请求。

那么解决图片乱序最核心的逻辑就在这里了,其实NetworkImageView的解决思路还是比较简单的,就是如果这个控件已经被移出了屏幕且被重新利用了,那么就把之前的请求取消掉,仅此而已。

而我们都知道,在通常情况下,仅仅这么处理可能是解决不了问题的,因为Java的线程无法保证一定可以中断,即使像第二种解决方案里使用的BitmapWorkerTask的cancel()方法,也不能保证一定可以把请求取消掉,所以还需要使用弱引用关联的处理方式。但是在NetworkImageView当中就可以这么任性,仅仅调用cancelRequest()方法把请求取消掉就可以了,这主要是得益于Volley的出色设计。由于Volley在网络方面的封装非常优秀,它可以保证,只要是取消掉的请求,就绝对不会进行回调,既然不会回调,那么也就不会回到NetworkImageView当中,自然也就不会出现乱序的情况了。

需要注意的是,Volley只是保证取消掉的请求不会进行回调而已,但并没有说可以中断任何请求。由此可见即使是Volley也无法做到中断一个正在执行的线程,如果有一个线程正在执行,Volley只会保证在它执行完之后不会进行回调,但在调用者看来,就好像是这个请求就被取消掉了一样。

那么这里我们只分析与图片乱序相关部分的源码,如果你想了解关于Volley更多的源码,可以参考我之前的一篇文章 Android Volley完全解析(四),带你从源码的角度理解Volley 。

这是第三种解决方案。

好了,关于ListView异步加载图片乱序的问题今天我们就讨论到这里,如果你把三种解决方案都理解清楚的话,那么对于这个问题研究的就算比较透彻了。下一篇文章仍然是ListView主题,我们将学习一下如何对ListView控件进行一些功能扩展,敬请期待。

[!--infotagslink--]

相关文章

  • Android子控件超出父控件的范围显示出来方法

    下面我们来看一篇关于Android子控件超出父控件的范围显示出来方法,希望这篇文章能够帮助到各位朋友,有碰到此问题的朋友可以进来看看哦。 <RelativeLayout xmlns:an...2016-10-02
  • C#创建自定义控件及添加自定义属性和事件使用实例详解

    这篇文章主要给大家介绍了关于C#创建自定义控件及添加自定义属性和事件使用的相关资料,文中通过示例代码介绍的非常详细,对大家学习或者使用C#具有一定的参考学习价值,需要的朋友们下面来一起学习学习吧...2020-06-25
  • JS实现自定义简单网页软键盘效果代码

    本文实例讲述了JS实现自定义简单网页软键盘效果。分享给大家供大家参考,具体如下:这是一款自定义的简单点的网页软键盘,没有使用任何控件,仅是为了练习JavaScript编写水平,安全性方面没有过多考虑,有顾虑的可以不用,目的是学...2015-11-08
  • Android开发中findViewById()函数用法与简化

    findViewById方法在android开发中是获取页面控件的值了,有没有发现我们一个页面控件多了会反复研究写findViewById呢,下面我们一起来看它的简化方法。 Android中Fin...2016-09-20
  • Android模拟器上模拟来电和短信配置

    如果我们的项目需要做来电及短信的功能,那么我们就得在Android模拟器开发这些功能,本来就来告诉我们如何在Android模拟器上模拟来电及来短信的功能。 在Android模拟...2016-09-20
  • 夜神android模拟器设置代理的方法

    夜神android模拟器如何设置代理呢?对于这个问题其实操作起来是非常的简单,下面小编来为各位详细介绍夜神android模拟器设置代理的方法,希望例子能够帮助到各位。 app...2016-09-20
  • android自定义动态设置Button样式【很常用】

    为了增强android应用的用户体验,我们可以在一些Button按钮上自定义动态的设置一些样式,比如交互时改变字体、颜色、背景图等。 今天来看一个通过重写Button来动态实...2016-09-20
  • Android WebView加载html5页面实例教程

    如果我们要在Android应用APP中加载html5页面,我们可以使用WebView,本文我们分享两个WebView加载html5页面实例应用。 实例一:WebView加载html5实现炫酷引导页面大多...2016-09-20
  • 深入理解Android中View和ViewGroup

    深入理解Android中View和ViewGroup从组成架构上看,似乎ViewGroup在View之上,View需要继承ViewGroup,但实际上不是这样的。View是基类,ViewGroup是它的子类。本教程我们深...2016-09-20
  • Android自定义WebView网络视频播放控件例子

    下面我们来看一篇关于Android自定义WebView网络视频播放控件开发例子,这个文章写得非常的不错下面给各位共享一下吧。 因为业务需要,以下代码均以Youtube网站在线视...2016-10-02
  • 自定义jquery模态窗口插件无法在顶层窗口显示问题

    自定义一个jquery模态窗口插件,将它集成到现有平台框架中时,它只能在mainFrame窗口中显示,无法在顶层窗口显示. 解决这个问题的办法: 通过以下代码就可能实现在顶层窗口弹窗 复制代码 代码如下: $(window.top.documen...2014-05-31
  • Android用MemoryFile文件类读写进行性能优化

    java开发的Android应用,性能一直是一个大问题,,或许是Java语言本身比较消耗内存。本文我们来谈谈Android 性能优化之MemoryFile文件读写。 Android匿名共享内存对外A...2016-09-20
  • Android设置TextView竖着显示实例

    TextView默认是横着显示了,今天我们一起来看看Android设置TextView竖着显示如何来实现吧,今天我们就一起来看看操作细节,具体的如下所示。 在开发Android程序的时候,...2016-10-02
  • 自定义feignClient的常见坑及解决

    这篇文章主要介绍了自定义feignClient的常见坑及解决方案,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教...2021-10-20
  • pytorch 自定义卷积核进行卷积操作方式

    今天小编就为大家分享一篇pytorch 自定义卷积核进行卷积操作方式,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...2020-05-06
  • android.os.BinderProxy cannot be cast to com解决办法

    本文章来给大家介绍关于android.os.BinderProxy cannot be cast to com解决办法,希望此文章对各位有帮助呀。 Android在绑定服务的时候出现java.lang.ClassCastExc...2016-09-20
  • vscode搭建STM32开发环境的详细过程

    这篇文章主要介绍了vscode搭建STM32开发环境的详细过程,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下...2021-05-02
  • Android 实现钉钉自动打卡功能

    这篇文章主要介绍了Android 实现钉钉自动打卡功能的步骤,帮助大家更好的理解和学习使用Android,感兴趣的朋友可以了解下...2021-03-15
  • PHP YII框架开发小技巧之模型(models)中rules自定义验证规则

    YII的models中的rules部分是一些表单的验证规则,对于表单验证十分有用,在相应的视图(views)里面添加了表单,在表单被提交之前程序都会自动先来这里面的规则里验证,只有通过对其有效的限制规则后才能被提交,可以很有效地保证...2015-11-24
  • Android 开发之布局细节对比:RTL模式

    下面我们来看一篇关于Android 开发之布局细节对比:RTL模式 ,希望这篇文章对各位同学会带来帮助,具体的细节如下介绍。 前言 讲真,好久没写博客了,2016都过了一半了,赶紧...2016-10-02