作者: 夏至
欢迎转载,也请保留这段申明,后附源码:
http://blog.csdn.net/u011418943/article/details/53013130
今天要实现的效果如下所示:
分析一下,就是简单的用一个ListView 去加载控件,这个没什么好说,很简单,然后我们获取到幕课网的sdk获取一些数据,加载进来,只不过我们这里的图片采用的是网络下载。
首先是ListView 的Item,这里先上布局 content.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"
android:padding="4dp"
android:orientation="horizontal" >
<ImageView
android:id="@+id/iv_icon"
android:layout_width="74dp"
android:layout_height="64dp"
android:src="@drawable/ic_launcher"
android:scaleType="fitXY"/>
<LinearLayout
android:layout_width="wrap_content"
android:paddingLeft="5dp"
android:layout_height="64dp"
android:orientation="vertical" >
<TextView
android:id="@+id/tv_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:singleLine="true"
android:ellipsize="end"
android:text="Tite"
android:textSize="18sp"/>
<TextView
android:id="@+id/tv_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingLeft="10dp"
android:maxLines="3"
android:text="content" />
</LinearLayout>
</LinearLayout>
然后是对这个Item的一个封装 Content.java:
package com.example.picturelurcache.bean;
public class Content {
private String tvTitle,tvContent;
private String ivIcon;
public String getTvTitle() {
return tvTitle;
}
public void setTvTitle(String tvTitle) {
this.tvTitle = tvTitle;
}
public String getTvContent() {
return tvContent;
}
public void setTvContent(String tvContent) {
this.tvContent = tvContent;
}
public String getIvIcon() {
return ivIcon;
}
public void setIvIcon(String ivIcon) {
this.ivIcon = ivIcon;
}
}
我们的住布局 activity_main.xml:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.example.picturelurcache.MainActivity" >
<ListView
android:id="@+id/lv"
android:layout_width="match_parent"
android:layout_height="wrap_content"
/>
</RelativeLayout>
然后我们让 我们的适配器去继承 BaseAdater:
public class ContentAdapter extends BaseAdapter{
private List<Content> mData;
private Context mContext;
private String mUrl;
public ContentAdapter(Context context,List<Content> data,String url){
mContext = context;
mData = data;
}
@Override
public int getCount() {
// TODO Auto-generated method stub
return mData.size();
}
@Override
public Object getItem(int arg0) {
// TODO Auto-generated method stub
return mData.get(arg0);
}
@Override
public long getItemId(int arg0) {
// TODO Auto-generated method stub
return arg0;
}
class ViewHolder{
ImageView ivIcon;
TextView tvTitle,tvContent;
}
@Override
public View getView(int arg0, View contentView, ViewGroup arg2) {
// TODO Auto-generated method stub
ViewHolder viewHolder = null;
if (contentView == null) {
contentView = LayoutInflater.from(mContext).inflate(R.layout.content, null);
viewHolder = new ViewHolder();
viewHolder.ivIcon = (ImageView) contentView.findViewById(R.id.iv_icon);
viewHolder.tvTitle = (TextView) contentView.findViewById(R.id.tv_title);
viewHolder.tvContent = (TextView) contentView.findViewById(R.id.tv_content);
contentView.setTag(viewHolder);
}else{
viewHolder = (ViewHolder) contentView.getTag();
}
return contentView;
}
}
这里的初始化,我们采用的用 List 来传递,因为我们不知道会有多少张,所以用List<> 是比较好的方法。然后是ListView的缓存优化,用到viewholder这个类。
接下来就是我们的主函数,首先数据时从网线下载的,这里我们用到 异步下载 AsyncTask,由于封装得比较好,我们这里就拿出来用了。
url 链接为幕课网的课程链接: http://www.imooc.com/api/teacher?type=4&num=30
所以,异步下载这么写:
class myDowndata extends AsyncTask<String, Integer, String> {
@Override
// 在 doInBackground 处理一些耗时的操作
protected String doInBackground(String... arg0) {
// TODO Auto-generated method stub
String string = GetJson.getJsonFromMK(arg0[0]);
return string;// 传入url,获取json数据;;
}
@Override
protected void onPostExecute(String result) {
// TODO Auto-generated method stub
super.onPostExecute(result);
}
}
可以看到我们在 doInBackground 处理我们获取的 json 数据,这里从网上获取读取数据,采用 HttpURLConnection 这个类:
public class GetJson {
public static String getJsonFromMK(String url) {
StringBuffer sb = null;
try {
URL httpUrl = new URL(url);
HttpURLConnection connection = (HttpURLConnection) httpUrl
.openConnection();
connection.setRequestMethod("GET");
connection.setConnectTimeout(5000);
BufferedReader br = new BufferedReader(new InputStreamReader(
connection.getInputStream(), "utf-8"));
sb = new StringBuffer();
String string = null;
while((string = br.readLine())!=null){
sb.append(string);
}
br.close();
} catch (MalformedURLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return sb.toString();
}
}
把返回的 gson 字符串,返回到onPostExecute()这里来处理:
所以,这里我们可以这么写:
protected void onPostExecute(String result) {
// TODO Auto-generated method stub
super.onPostExecute(result);
ContentAdapter adapter = new ContentAdapter(MainActivity.this, disposeGson(result),url);
mListView.setAdapter(adapter);
}
其中,disposeGson 为json 数据的处理,采用的是 Gson的框架包,我们需要把图片和文字返回,所以类型也用 List ,具体函数如下:
private List<Content> disposeGson(String json) {
List<Content> mContents = new ArrayList<Content>();//新建一个 List<Content>
Gson gson = new Gson();
java.lang.reflect.Type type = new TypeToken<Root>() {
}.getType();
Root root = gson.fromJson(json, type); //解析json
//
if (root != null) {
List<Data> data = root.getData();
for (int i = 0; i < data.size(); i++) {
Content content = new Content(); //List.add加入的是new 的新对象,所以要放在这里
// 把获取到的json 数据导进去
content.setTvTitle(data.get(i).getName());
content.setTvContent(data.get(i).getDescription());
content.setIvIcon(data.get(i).getPicSmall());
mContents.add(content); //把这个泛型存进来
}
}
return mContents;
}
为什么这么写,因为返回的json数据格式如下:
非常简单,至于数据的封装,就不贴出来了。可以看后面的工程。
ok,那么上面的 ListView 应该这么写:
public View getView(int arg0, View contentView, ViewGroup arg2) {
// TODO Auto-generated method stub
ViewHolder viewHolder = null;
if (contentView == null) {
contentView = LayoutInflater.from(mContext).inflate(R.layout.content, null);
viewHolder = new ViewHolder();
viewHolder.ivIcon = (ImageView) contentView.findViewById(R.id.iv_icon);
viewHolder.tvTitle = (TextView) contentView.findViewById(R.id.tv_title);
viewHolder.tvContent = (TextView) contentView.findViewById(R.id.tv_content);
contentView.setTag(viewHolder);
}else{
viewHolder = (ViewHolder) contentView.getTag();
}
viewHolder.ivIcon.setImageResource(R.drawable.ic_launcher);
viewHolder.tvTitle.setText(mData.get(arg0).getTvTitle());
viewHolder.tvContent.setText(mData.get(arg0).getTvContent());
return contentView;
}
细心的你可能发现,这里的图片,我们用的是原始图片,而不是从网络下载的。因为图片毕竟不是文字,而且,从json数据中,我们看到图片给的是一个 url 的链接,所以还是需要我们从网络下载的,因此我们新建一个类:
首先,以线程的方式下载:
public class getImageFromHttp {
private ImageView mImageView;
private String mUrl;
private Handler mHandler = new Handler() {
public void handleMessage(android.os.Message msg) {
mImageView.setImageBitmap((Bitmap) msg.obj);
};
};
public void showBitmap(ImageView imageView, final String url) {
mImageView = imageView;
mUrl = url;
new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
Message msg = new Message();
msg.obj = getBitmapFromURL(url);
mHandler.sendMessage(msg);
try {
Thread.sleep(1000); //延时1s用来延时网络不好的情况
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}).start();
}
}
非常简单,然后在我们的ContentAdater 那里添加这一句:
new getImageFromHttp().showBitmap(viewHolder.ivIcon, iconUrl);
viewHolder.tvTitle.setText(mData.get(arg0).getTvTitle());
....
发现,已经有我们想要的效果了,但是可以发现有些图片多次下载了很多,这是为什么呢?
因为我们在上面有个viewholder类,用来缓存,避免重复加载的,当我们第一次的是为空的,就新建一个,但ListView 只显示当前页面的子 Item,那么它就会从这个缓存中拿出来用,而拿出来用,它并知道如何区分,所以,这就是造成复用的最大原因。
if (contentView == null)
当它滑动再次滑动,那么被移除当前可视部分的,就会被缓存起来,快速滑动,Item 就会被从缓存中拿出来,但这个时候会重新下载,就会被下载多次,重新来。
所以,这里我们的处理方法就是,加一个 TAG。给一个身份,都一样的采取下载:
String iconUrl = mData.get(arg0).getIvIcon();
viewHolder.ivIcon.setTag(iconUrl);
new getImageFromHttp().showBitmap(viewHolder.ivIcon, iconUrl);
...
完整的getView 如下:
public View getView(int arg0, View contentView, ViewGroup arg2) {
// TODO Auto-generated method stub
ViewHolder viewHolder = null;
if (contentView == null) {
contentView = LayoutInflater.from(mContext).inflate(R.layout.content, null);
viewHolder = new ViewHolder();
viewHolder.ivIcon = (ImageView) contentView.findViewById(R.id.iv_icon);
viewHolder.tvTitle = (TextView) contentView.findViewById(R.id.tv_title);
viewHolder.tvContent = (TextView) contentView.findViewById(R.id.tv_content);
contentView.setTag(viewHolder);
}else{
viewHolder = (ViewHolder) contentView.getTag();
}
String iconUrl = mData.get(arg0).getIvIcon();
viewHolder.ivIcon.setTag(iconUrl);
viewHolder.ivIcon.setImageResource(R.drawable.ic_launcher);
//new getImageFromHttp().showBitmap(viewHolder.ivIcon, iconUrl);
new getImageFromHttp().showBitmapByAsyncTask(viewHolder.ivIcon, iconUrl);
viewHolder.tvTitle.setText(mData.get(arg0).getTvTitle());
viewHolder.tvContent.setText(mData.get(arg0).getTvContent());
return contentView;
}
在图片更新下载那里,我们要这样判断:
private Handler mHandler = new Handler() {
public void handleMessage(android.os.Message msg) {
if (mImageView.getTag().equals(mUrl)) { // 设置tag 的原因在于 listview
// 的缓存机制,刷新时没有正确刷新,所以这里使用tag来表示
mImageView.setImageBitmap((Bitmap) msg.obj);
}
};
};
当然,你也用异步下载的方式:
public void showBitmapByAsyncTask(ImageView imageview,String url){
new newAsyncTask(imageview,url).execute(url);
}
private class newAsyncTask extends AsyncTask<String, Void, Bitmap>{
public newAsyncTask(ImageView imageView,String url){
mImageView = imageView;
mUrl = url;
}
@Override
protected Bitmap doInBackground(String... arg0) {
// TODO Auto-generated method stub
return getBitmapFromURL(arg0[0]);
}
@Override
protected void onPostExecute(Bitmap result) {
// TODO Auto-generated method stub
super.onPostExecute(result);
if (mImageView.getTag().equals(mUrl)) {
mImageView.setImageBitmap(result);
}
}
}
二 使用 lruCache 缓存
当网络比较差,或者说每次都要重新下载那些图片,不仅耗流量,而且用户体验特别差。所以,这个时候,我们就好的办法就是使用缓存的方式。
lruCache 使用非常简单,就是一个 key-value 表。所以首先,我们先创建一个 Cache
private LruCache< String, Bitmap> mCache;//这里的可以对应 url,value对应Bitmap
public getImageFromHttp(){
//最大可用内存
int maxSize = (int) Runtime.getRuntime().maxMemory();
int memSize = maxSize/8;
mCache = new LruCache<String,Bitmap>(maxSize){
protected int sizeOf(String key, Bitmap value) { //默认返回时value 的个数
//这里我们返回bitmap的大小
return value.getByteCount();
}
};
}
创建两个方法,用来检查缓存是否已经有value了。
/**
* 从缓存中获取图片
* @param key
* @return
*/
private Bitmap getBitmapFromCache(String key){
return mCache.get(key);
}
/**
* 把图片放到缓存
* @param key
* @param bitmap
*/
private void setBitmapToCache(String key,Bitmap bitmap){
if (getBitmapFromCache(key) == null) { //缓存中没有才创建
mCache.put(key, bitmap);
}
}
我们用asyncTask 来演示一下:
public void showBitmapByAsyncTask(ImageView imageview,String url){
//如果这个缓存中并没有这个图片,则直接下载,有则用缓存的
Bitmap bitmap = getBitmapFromCache(url);
if (bitmap == null) {
new newAsyncTask(imageview,url).execute(url);
}else{
imageview.setImageBitmap(bitmap);
}
}
protected Bitmap doInBackground(String... arg0) {
// TODO Auto-generated method stub
Bitmap bitmap = getBitmapFromURL(arg0[0]); //从网络下载的图片
if (bitmap != null) {
setBitmapToCache(arg0[0], bitmap);
}
return bitmap;
}
上面的思路是:
- 下载之前判断是否缓存中已经有这张图片了,若有,则直接获取,若无,则下载
- 当下载的时候,也把我们的图片放到我们的缓存中。
这里的话,我们还要改一下 ContentAdater.java:
三、优化
接下来我们继续优化,当我们的listview 比较复杂,或者说内容比较多,需要下载的东西也比较多,这个时候,我们就需要这样处理了,当滑动的时候,不让控件去加载东西,保证流畅度,当滑动完成的时候,才让它去加载任务。所以,我们的ContentAdapter 需要加进来一个listview 来判断它的状态:
然后,我们来看它的两个方法:
//可见部分
public void onScroll(AbsListView arg0, int first, int count, int arg3) {
// TODO Auto-generated method stub
mStart = first;
mEnd = first+count;
}
// 滑动的时候
public void onScrollStateChanged(AbsListView arg0, int status) {
//当listview 静止的时候才会去下载
if (status == SCROLL_STATE_FLING) {
//加载图片
}else{ //滑动的时候取消所有的任务。
}
}
至于 first 和 count 呢?是 listview 的可见部分,所以,这里也是我们要的。那图片显示怎么搞呢?我们需要在图片下载那里写个方法:
/**
* 用来下载图片
* @param start
* @param end
*/
public void showImageLoad(int start,int end){
for (int i = start; i < end; i++) {
String url = ContentAdapter.mUrlImage[i];
//如果这个缓存中并没有这个图片,则直接下载,有则用缓存的
Bitmap bitmap = getBitmapFromCache(url);
if (bitmap == null) {
//从showBitmapByAsyncTask中拿回下载的主动权
newAsyncTask task = new newAsyncTask(url);
task.execute(url);
mTasks.add(task); //把task 加进来
} else {
adapter = (ContentAdapter) mListView.getAdapter();
//如果缓冲有,则直接赋值给listview,直接显示
ImageView imageview = (ImageView) mListView.findViewWithTag(url);
if (imageview != null) {
imageview.setImageBitmap(bitmap);
adapter.notifyDataSetChanged();
}
}
}
}
思路是什么?就是,我只加载可见的部分,即我们上面的start和end,然后把下载的部分从showBitmapByAsyncTask 中转移到showImageLoad,然后如果缓存中没有这个图片,就重新下载,然后显示,如果有,则直接显示。
图片的显示部分,可以直接通过 listview.findViewWithTag();这个函数,因为上面我们已经对每一个ImageView 设置了Tag,所以这里就可以直接用了,也少了判断。
然后,我们需要用到 adapter.notifyDataSetChanged()来刷新我们的数据。
我们的AsyncTask 也稍微要变一下:
private class newAsyncTask extends AsyncTask<String, Void, Bitmap>{
public newAsyncTask(String url){
// mImageView = imageView;
adapter = (ContentAdapter) mListView.getAdapter();
mUrl = url;
}
@Override
protected Bitmap doInBackground(String... arg0) {
// TODO Auto-generated method stub
Bitmap bitmap = getBitmapFromURL(arg0[0]);
if (bitmap != null) {
setBitmapToCache(arg0[0], bitmap);
}
return bitmap;
}
@Override
protected void onPostExecute(Bitmap bitmap) {
// TODO Auto-generated method stub
super.onPostExecute(bitmap);
ImageView imageView = (ImageView) mListView.findViewWithTag(mUrl);
if (imageView !=null && bitmap!=null) {
imageView.setImageBitmap(bitmap);
adapter.notifyDataSetChanged();
}
}
}
可以看到就 onPostExecute 与上面的不同,其他的都不变。
函数已经写好了,所以ContentAdapter 应该这样写:
//可见部分
public void onScroll(AbsListView arg0, int first, int count, int arg3) {
// TODO Auto-generated method stub
mStart = first;
mEnd = first+count;
if (mfirstStart && arg3>0) { //第一次不滑动的时候,也加载
mfirstStart = false;
mGetImageFromHttp.showImageLoad(mStart, mEnd);
}
}
// 滑动的时候
public void onScrollStateChanged(AbsListView arg0, int status) {
//当listview 静止的时候才会去下载
if (status == SCROLL_STATE_IDLE) {
mGetImageFromHttp.showImageLoad(mStart, mEnd);
}else{ //滑动的时候取消所有的任务。
mGetImageFromHttp.cancelTask();
}
}
注意!如果你是用模拟器,记得用鼠标拖动,如果用中间滑轮,是不会刷新的。
附上源码:http://download.csdn.net/detail/u011418943/9672487
不用积分,也希望能帮到大家,谢谢。