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

前一阵看了些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的高度以及对启动图片下载任务的时机的修改,做到了只下载所要查看到的图片,避免多余的网络请求,减少了网络请求的负荷和节省了手机流量。

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

参考:

使用内存缓存实现图片墙

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

时间: 2024-11-08 17:18:28

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

ASP.NET Aries 入门开发教程2:配置出一个简单的列表页面

前言: 朋友们都期待我稳定地工作,但创业公司若要躺下,也非意念可控. 若人生注定了风雨飘摇,那就雨中前行了. 最机开始看聊新的工作机会,欢迎推荐,创业公司也可! 同时,趁着自由时间,抓紧把这系列教程给写完了. 谢谢大伙的关注和支持. 步骤1:准备好数据库和表(或视图) 由于框架支持跨数据库,所以可以先不用管系统权限的那个数据库,先随意找个数据库. 当然你也可以找个视图(只是视图就不能编辑或删除[权限控制]) 步骤2:配置数据库链接: 以数据库名+Conn 为name(这是跨库的约定,不要乱取).

angularjs 的一个图片列表指令

这是一个基于angulsrjs 和bootstrap样式的图片列表指令 js部分代码如下 var app=angular.module("test",["bane/image/showgrid.html"]) .directive("baneImggrid",function(){ return { restrict:'EA', transclude: true, replace: false, //templateUrl: "temp

Android高级控件(五)——如何打造一个企业级应用对话列表,以QQ,微信为例

Android高级控件(五)--如何打造一个企业级应用对话列表,以QQ,微信为例 看标题这么高大上,实际上,还是运用我么拿到listview去扩展,我们讲什么呢,就是研究一下QQ,微信的这种对话列表,我们先看一个传统的ListView是怎么样的,我们做一个通讯录吧,通讯录的组成就是一个头像,一个名字,一个电话号码,一个点击拨打的按钮,既然这样,那我们的item就出来了 call_list_item.xml <?xml version="1.0" encoding="ut

非等宽图片列表的布局

各大搜索引擎的图片频道的搜索结果页,搜索出来的结果都是较零碎的图片,图片质量.尺寸都是参差不齐的,并限定了每一行的总宽度.这种非等宽的图片列表,在Google+.flickr也都有用到. 最近刚好对360搜索的图片搜索结果页进行了一次重构和改版,对于这种图片布局也花心思研究了一番,接下来说说我的一些处理思路. 非等宽的单个图片要排列到一个固定了宽度的容器中,那么这个等宽的容器就是最大的限制和障碍,开始怀念那种常见等宽瀑布流的布局(没有限制真好). 先理下基本的需求: 1.图片的宽度是不固定的:

打造支持apk下载和html5缓存的 IIS(配合一个超简单的android APP使用)具体解释

为什么要做这个看起来不靠谱的东西呢? 由于刚学android开发,还不能非常好的熟练控制android界面的编辑和操作,所以我的一个急着要的运用就改为html5版本号了,反正这个运用也是须要从server获取大量数据来展示在手机上面的,也就是说:必须联网,才干正常工作,于是想了一下,反正都要联网获取数据,为什么不直接用我相对熟悉一点的 html来做这个运用呢?省的花费不够用的时间去学习android界面的控制,于是就简单了:用蹩脚的手段做了一个android程序的启动欢迎界面,内页就是一个全屏的

论字母导航的重要性,我们来实现一个联系人字母导航列表吧!

论字母导航的重要性,我们来实现一个联系人字母导航列表吧! 说起这个字母导航,我相信大家都不陌生,不论是联系人列表还是城市列表,基本上都是需要字母导航,那我们就有必要来研究一下这个思路的探索了,毕竟这是一个非常常用的功能,如果现在把轮子造好,那以后也可以节省很多的时间,同时,我们把思路理清楚了,对我们以后的帮助也是很大的,那好,既然如此,我们就一起来探索一下吧! 我们首选新建一个项目--LettersNavigation OK,工程建立好之后我们来思考一下这个功能的一个实现逻辑 逻辑不是很难,那我

WPF自定义控件与样式(12)-缩略图ThumbnailImage /gif动画图/图片列表

原文:WPF自定义控件与样式(12)-缩略图ThumbnailImage /gif动画图/图片列表 一.前言 申明:WPF自定义控件与样式是一个系列文章,前后是有些关联的,但大多是按照由简到繁的顺序逐步发布的等,若有不明白的地方可以参考本系列前面的文章,文末附有部分文章链接. 本文主要针对WPF项目开发中图片的各种使用问题,经过总结,把一些经验分享一下.内容包括: WPF常用图像数据源ImageSource的创建: 自定义缩略图控件ThumbnailImage,支持网络图片.大图片.图片异步加载

C# 系统应用之ListView实现简单图片浏览器

最近有同学问我如何使用ListView加载图片列表,前面在"C#系统应用"中TreeView+ListView+ContextMenuStrip控件实现树状图显示磁盘目录,并在ListView中显示文件的详细信息.这里准备简单介绍下给同学讲述的如何使用ListView+ImageList控件实现简单的图片浏览器知识.        第一步 设计界面框架如下图所示,同时添加ImageList控件(不可见) 注意:设置ListView控件的Anchor属性为Top,Bottom,Right

[读码时间] 图片列表:鼠标移入/移出改变图片透明度

说明:代码来自网络.注释为笔者学习时添加. <!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>图片列表:鼠标移入/移出改变图片透明度</title> <style> ul,li{ /*去除内外边距,去除列表默认样式*/ margin:0; padding:0; list-style-type:none; } #imgList{ /