码迷,mamicode.com
首页 > 其他好文 > 详细

实现一个最简单图片列表所引发的问题

时间:2015-07-21 14:46:42      阅读:234      评论:0      收藏:0      [点我收藏+]

标签:

前一阵看了些Universal-Image-Loager的源码。我觉得看源码很累的一个原因就是除了看怎么实现,就是去揣测为什么这么实现。这个揣测的过程很容易走马观花,看到后面似懂非懂。
人懒到一个地步一句话来说是能躺着就绝对不坐着,能坐着就绝对不蹲着,能蹲着就绝对不站着。有时候看源码也是,能看懂就不会想着去debug,debug能看明白的就懒得去动手写写。

看和写的感受是不一样的。看的是结果,写的是过程。

第三方库的使用让开发变得很方便,大量图片请求的实现,大多数不再是说实现的核心,而是直接说使用什么第三方。即便如此也没关系,只要知道别人是怎么实现的就好了,这很重要。Universal-Image-Loader是一个强大的图片加载开源框架,应该都被说烂用烂了吧。这篇主要是为了去发现问题,从0开始。

看代码的时候很多为什么,为什么要这么写,怎么就想到会有这些问题,怎么就想到用这种方法去解决。想找原因还是从最简单的实现一个图片列表开始找吧。不用任何框架,也不考虑什么缓存,单纯的去写一个网络请求显示图片列表这样一个功能。


新建一个PhotoListActivity,这个类只显示一个listview:

package com.aliao.learninguil.activity;

import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.widget.ListView;

import com.aliao.learninguil.Constants;
import com.aliao.learninguil.R;
import com.aliao.learninguil.adapter.PhotoListAdapter;
import com.aliao.learninguil.entity.ImageInfo;

import java.util.ArrayList;
import java.util.List;

/**
 * Created by ALiao on 2015/7/13.
 */
public class PhotoListActivity extends AppCompatActivity {

    private ListView mListView;
    private PhotoListAdapter mAdapter;
    private List<ImageInfo> mImageInfos = new ArrayList<>();
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_photolist);
        for (int i = 0; i< Constants.IMAGES.length; i++){
            ImageInfo imageInfo = new ImageInfo();
            imageInfo.setUrl(Constants.IMAGES[i]);
            imageInfo.setName("item-" + i);
            mImageInfos.add(imageInfo);
        }
        mListView = (ListView) findViewById(R.id.photoList);
        mAdapter = new PhotoListAdapter(mImageInfos, mListView);
        mListView.setAdapter(mAdapter);
    }
}
listview的item的布局是左边显示一个ImageVeiw,右边显示一个textview:

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

    <ImageView
        android:id="@+id/iv_img"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

    <TextView
        android:id="@+id/tv_imagename"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

</LinearLayout>

要想在listview的每个item中显示一张网络请求的图片,那么在PhotoListAdapter中要启动线程进行网络请求。

package com.aliao.learninguil.adapter;

import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.AsyncTask;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.TextView;

import com.aliao.learninguil.R;
import com.aliao.learninguil.entity.ImageInfo;
import com.aliao.learninguil.utils.L;

import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.List;

public class PhotoListAdapter extends BaseAdapter {

    private List<ImageInfo> imageInfos;
    private ListView mListView;

    public PhotoListAdapter(List<ImageInfo> imageInfos, ListView listView) {
        this.imageInfos = imageInfos;
        mListView = listView;
    }

    @Override
    public int getCount() {
        return imageInfos.size();
    }

    @Override
    public Object getItem(int position) {
        return imageInfos.get(position);
    }

    @Override
    public long getItemId(int position) {
        return position;
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {

        ViewHolder holder;
        if (convertView == null){
            convertView = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_photolist, parent, false);
            holder = new ViewHolder();
            holder.imgView = (ImageView) convertView.findViewById(R.id.iv_img);
            holder.imgName = (TextView) convertView.findViewById(R.id.tv_imagename);
            convertView.setTag(holder);
        }else {
            holder = (ViewHolder) convertView.getTag();
        }

        ImageInfo imageInfo = imageInfos.get(position);
        holder.imgName.setText(imageInfo.getName());
        holder.imgView.setTag(imageInfo.getUrl());
        loadAndSetImage(imageInfo.getUrl());

        return convertView;
    }

    class ViewHolder{
        ImageView imgView;
        TextView imgName;
    }

    private void loadAndSetImage(String url) {
        new LoadImageAsyncTask().execute(url);
    }

    class LoadImageAsyncTask extends AsyncTask<String, Void, Bitmap>{

        private String mImageUrl;

        @Override
        protected Bitmap doInBackground(String... params) {

            mImageUrl = params[0];

            Bitmap bitmap = loadBitmap(mImageUrl);

            return bitmap;
        }

        @Override
        protected void onPostExecute(Bitmap bitmap) {
            super.onPostExecute(bitmap);
            ImageView imageView = (ImageView) mListView.findViewWithTag(mImageUrl);
            if (imageView != null){
                imageView.setImageBitmap(bitmap);
            }
        }
    }

    private Bitmap loadBitmap(String imageUrl) {
        HttpURLConnection connection = null;
        Bitmap bitmap = null;
        try {
            URL url = new URL(imageUrl);
            connection = (HttpURLConnection) url.openConnection();
            bitmap = BitmapFactory.decodeStream(connection.getInputStream());
            return bitmap;
        } catch (MalformedURLException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            if (connection != null){
                connection.disconnect();
            }
        }
        return bitmap;
    }

}
这是实现一个图片列表最基本的代码了。实现的效果图如下:

技术分享

看起来显示的效果还不错,但是当我们上下滑动的时候明显会感受到不够流畅。

回到PhotoListAdapter类,每调用一次getView方法,就会去启动线程进行网络请求。我们期望的效果是当页面显示5个item的时候,就进行5次网络请求,这是最理想的状态。但是实际上getView调用的次数比预想的要多得多,也就意味着会伴随多余的网络请求。有哪些情况会导致多余的网络请求呢(这里的多余是表示,除了当前屏幕显示的图片以外,进行了额外的其他图片的网络请求)

情况一:没有设置item的高度

listview item的布局文件中看到,并没有去设置ImageView的高度,item的高度是自动扩展的:

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

    <ImageView
        android:id="@+id/iv_img"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

    <TextView
        android:id="@+id/tv_imagename"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

</LinearLayout>
所以,当进入图片列表页时,由于还未加载出图片,默认是textview的高度,一屏能够显示了27个item,即调用了27次getView,也就是开启线程进行网络请求的操作就要执行27次。但随着前面图片陆续加载完毕,一屏最终显示5个item,但是剩余的22次网络请求还在继续。通常我们希望一屏显示多少张图片就去请求多少张,当要看更多的图片的时候再去请求。而这22次网络请求既然都执行了,是不是往下滑动的时候他就可以直接显示出已加载完的图片?答案是否定的,因为除了当前屏幕显示的item,其他的item都被回收了,通过findViewWithTag(imageUrl)已经找不到对应的imageView(为null)。即使图片已经请求成功,但是由于当前屏幕没有对应的imageview,也无法设置图片。

        @Override
        protected void onPostExecute(Bitmap bitmap) {
            super.onPostExecute(bitmap);
            ImageView imageView = (ImageView) mListView.findViewWithTag(mImageUrl);
            if (imageView != null){
                imageView.setImageBitmap(bitmap);
            }
        }
所以继续向下滑的时候,还是会重复请求先前已经请求过的图片。

技术分享技术分享

如果提前已知item的显示高度,例如设置ImageView的高度为100dp或者设置默认图片,那么一屏能够显示的item数量是确定的,就可以做到一屏显示多少个item,进行相应数量的网络请求。


情况二:如果想看第三屏的图片列表,滑过去的前两屏的图片已经进行了网络请求,但并没必要

对于我们不想查看而快速滑过的图片是没有必要浪费资源去加载的。但是由于把加载图片的操作放在了getView()中,只要滑动屏幕都会调用getView(),我们希望当listview滑动停止时再去加载。listview的滚动监听事件的回调函数可以监听到滚动的状态,onScrollStateChanged(AbsListView view, int scrollState)中的scrollState有三种表示listvuew的滚动状态,分别是:

SCROLL_STATE_IDLE( = 0 )  :停止滚动

SCROLL_STATE_TOUCH_SCROLL( = 1 )  :正在滚动

SCROLL_STATE_FLING( = 2 )  :手指做了抛的动作

当scrollState的状态为SCROLL_STATE_IDLE的时候,去下载图片。图片下载的时机确定了,那么当滑动停止时当前屏幕显示的可视图片的地址该怎么获取。imageInfos对象列表存放了所有图片信息,知道了当前屏幕可视图片的position位置,也就可以通过索引获取到图片地址了。另一个滚动监听的回调onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount)(滚动时一直回调)帮助我们获取到当前屏幕图片的位置信息。参数中

firstVisibleItem  :当前屏幕第一张可见item的下标(从0开始)

visibleItemCount  :当前屏幕所有可见item的总数 

totalItemCount  :列表项总数

具体的代码实现如下:

package com.aliao.learninguil.adapter;

import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.AsyncTask;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.widget.AbsListView;
import android.widget.BaseAdapter;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.TextView;

import com.aliao.learninguil.R;
import com.aliao.learninguil.entity.ImageInfo;
import com.aliao.learninguil.utils.L;

import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

public class PhotoListAdapter extends BaseAdapter implements AbsListView.OnScrollListener{

    private List<ImageInfo> imageInfos;
    private ListView mListView;
    private int mFirstVisibleItem;
    private int mVisibleItemCount;
    private boolean mFirstEnter;
    private Set<LoadImageAsyncTask> taskCollection;

    public PhotoListAdapter(List<ImageInfo> imageInfos, ListView listView) {
        this.imageInfos = imageInfos;
        mListView = listView;
        mListView.setOnScrollListener(this);
        mFirstEnter = true;
        taskCollection = new HashSet<>();
    }

    @Override
    public int getCount() {
        return imageInfos.size();
    }

    @Override
    public Object getItem(int position) {
        return imageInfos.get(position);
    }

    @Override
    public long getItemId(int position) {
        return position;
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {

        ViewHolder holder;
        if (convertView == null){
            convertView = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_photolist, parent, false);
            holder = new ViewHolder();
            holder.imgView = (ImageView) convertView.findViewById(R.id.iv_img);
            holder.imgName = (TextView) convertView.findViewById(R.id.tv_imagename);
            convertView.setTag(holder);
        }else {
            holder = (ViewHolder) convertView.getTag();
        }

        ImageInfo imageInfo = imageInfos.get(position);
        holder.imgName.setText(imageInfo.getName());
        holder.imgView.setTag(imageInfo.getUrl());
//        L.d("position = "+position+", url = "+imageInfo.getUrl());
//        loadAndSetImage(imageInfo.getUrl());

        return convertView;
    }


    class ViewHolder{
        ImageView imgView;
        TextView imgName;
    }


    @Override
    public void onScrollStateChanged(AbsListView view, int scrollState) {
        /**
         * scrollState = SCROLL_STATE_IDLE( = 0 )停止混动
         * scrollState = SCROLL_STATE_TOUCH_SCROLL( = 1 )正在滚动
         * scrollState = SCROLL_STATE_FLING( = 2 ) 手指做了抛的动作
         */
        L.d("-------》onScrollStateChanged scrollState = "+scrollState);
        if (scrollState == SCROLL_STATE_IDLE){
            loadAndSetImage(mFirstVisibleItem, mVisibleItemCount);
        }else {
            //当listview再次滑动时取消所有正在下载的任务
//            cancelAllTasks();
        }
    }

    public void cancelAllTasks() {
        if (taskCollection != null){
            for (LoadImageAsyncTask task : taskCollection){
                task.cancel(false);
            }
        }
    }

    @Override
    public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
        L.d("onScroll firstVisibleItem = "+firstVisibleItem+", visibleItemCount = "+visibleItemCount+", totalItemCount = "+totalItemCount);
        mFirstVisibleItem = firstVisibleItem;//第一张可见图片的下标
        mVisibleItemCount = visibleItemCount;//一屏可见图片的总数

        //首次进入程序时,onScrollStateChanged方法并不会被调用,所以在这里首次进入程序时启动下载任务
        if (mFirstEnter && visibleItemCount > 0){
            loadAndSetImage(mFirstVisibleItem, mVisibleItemCount);
            mFirstEnter = false;
        }

    }

    private void loadAndSetImage(int firstVisibleItem, int visibleItemCount) {
        for (int i = firstVisibleItem; i< firstVisibleItem + visibleItemCount; i++){
            ImageInfo imageInfo = imageInfos.get(i);
            L.d("position = "+i+", url = "+imageInfo.getUrl());
            LoadImageAsyncTask task = new LoadImageAsyncTask();
            task.execute(imageInfo.getUrl());
            taskCollection.add(task);
        }
    }

    class LoadImageAsyncTask extends AsyncTask<String, Void, Bitmap>{

        private String mImageUrl;

        @Override
        protected Bitmap doInBackground(String... params) {

            mImageUrl = params[0];

            Bitmap bitmap = loadBitmap(mImageUrl);

            return bitmap;
        }

        @Override
        protected void onPostExecute(Bitmap bitmap) {
            super.onPostExecute(bitmap);
            ImageView imageView = (ImageView) mListView.findViewWithTag(mImageUrl);
            if (imageView != null){
                imageView.setImageBitmap(bitmap);
            }
        }
    }

    private Bitmap loadBitmap(String imageUrl) {
        HttpURLConnection connection = null;
        Bitmap bitmap = null;
        try {
            URL url = new URL(imageUrl);
            connection = (HttpURLConnection) url.openConnection();
            bitmap = BitmapFactory.decodeStream(connection.getInputStream());
            L.d("------------------------------------loadBitmap ");
            return bitmap;
        } catch (MalformedURLException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            if (connection != null){
                connection.disconnect();
            }
        }
        return bitmap;
    }

}
除了当listview再次滑动的时候,取消所有正在下载的任务,当Activity销毁时,在onDestroy方法中可以调用PhotoListAdapter中的cancelAllTasks()来取消所有还未完成的下载任务。

到目前为止通过确定list item的高度以及对启动图片下载任务的时机的修改,做到了只下载所要查看到的图片,避免多余的网络请求,减少了网络请求的负荷和节省了手机流量。

解决了上面的问题后,代码又完善健壮了一步,想想接下来还会有什么问题。


参考:

使用内存缓存实现图片墙




版权声明:本文为博主原创文章,未经博主允许不得转载。

实现一个最简单图片列表所引发的问题

标签:

原文地址:http://blog.csdn.net/aliaooooo/article/details/46964811

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!