Android开发实战《智慧北京》——6.项目完成 & 优化

1.组图布局 & 请求网格数据

之前我们完成了侧边栏中“新闻”的布局,现在需要侧边栏中实现“组图”的布局并请求网格数据。由于这个功能同时拥有ListView和GridView,所以可以考虑底层布局用FrameLayout来实现

  1. 新增paper_photos_menu_detail.xml布局文件,作为“组图”的根布局,代码如下:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <ListView
        android:id="@+id/lv_list2"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:divider="@null"/>

    <GridView
        android:id="@+id/gv_list"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:visibility="gone"
        android:numColumns="2"/>

</FrameLayout>
  1. 修改PhotosMenuDetailPaper,应用刚刚创建好的布局,并获取到实例对象,代码如下:
package com.example.zhbj.base.impl.menudetail;

import android.app.Activity;
import android.view.View;
import android.widget.GridView;
import android.widget.ListView;

import com.example.zhbj.R;
import com.example.zhbj.base.BaseMenuDetailPaper;
import com.lidroid.xutils.ViewUtils;
import com.lidroid.xutils.view.annotation.ViewInject;

/**
 * 菜单详情页 - 组图
 */
public class PhotosMenuDetailPaper extends BaseMenuDetailPaper {

    /**
     * ListView实例对象
     */
    @ViewInject(R.id.lv_list2)
    private ListView lvList;

    /**
     * GridView实例对象
     */
    @ViewInject(R.id.gv_list)
    private GridView gvList;

    public PhotosMenuDetailPaper(Activity activity) {
        super(activity);
    }

    @Override
    public View initViews() {
        // 给空的帧布局动态添加布局对象
        /*
        TextView view = new TextView(mActivity);
        view.setTextSize(22);
        view.setTextColor(Color.RED);
        view.setGravity(Gravity.CENTER); // 居中显示
        view.setText("菜单详情页-组图");
         */
        View view = View.inflate(mActivity, R.layout.paper_photos_menu_detail,null);
        ViewUtils.inject(this,view);
        return view;
    }
}
  1. 修改GlobalConstans,添加一个新的URL常量,表示组图的接口地址,代码如下:
package com.example.zhbj.global;

public class GlobalConstans {

    /**
     * 服务器根域名,地址
     */
    public static final String SERVER_URL = "http://10.0.2.2:8080/zhbj";

    /**
     * 分类接口地址
     */
    public static final String CATEGORY_URL =  SERVER_URL + "/categories.json";

    /**
     * 组图接口地址
     */
    public static final String PHOTOS_URL =  SERVER_URL + "/photos/photos_1.json";
}

  1. 在domain包下新建PhotosBean类,用于将获取到的JSON数据解析出来,代码如下:
package com.example.zhbj.domain;

import java.util.ArrayList;

/**
 * 组图网络数据
 */
public class PhotosBean {

    public PhotosData data;

    public class PhotosData{
        public ArrayList<PhotoNews> news;
    }

    public class PhotoNews{
        public String title;
        public String listimage;
    }
}
  1. 修改PhotosMenuDetailPaper,添加initData()方法,用于将数据进行初始化,再添加getDataFromServer()方法,用于从服务器上获取数据,再添加processData()方法,用于加载数据。除此之外,再通过缓存工具类来读/写缓存,代码如下:
package com.example.zhbj.base.impl.menudetail;

import android.app.Activity;
import android.text.TextUtils;
import android.view.View;
import android.widget.GridView;
import android.widget.ListView;

import com.example.zhbj.R;
import com.example.zhbj.base.BaseMenuDetailPaper;
import com.example.zhbj.domain.PhotosBean;
import com.example.zhbj.global.GlobalConstans;
import com.example.zhbj.util.CacheUtil;
import com.google.gson.Gson;
import com.lidroid.xutils.HttpUtils;
import com.lidroid.xutils.ViewUtils;
import com.lidroid.xutils.exception.HttpException;
import com.lidroid.xutils.http.ResponseInfo;
import com.lidroid.xutils.http.callback.RequestCallBack;
import com.lidroid.xutils.http.client.HttpRequest;
import com.lidroid.xutils.view.annotation.ViewInject;

import java.util.ArrayList;

/**
 * 菜单详情页 - 组图
 */
public class PhotosMenuDetailPaper extends BaseMenuDetailPaper {

    /**
     * ListView实例对象
     */
    @ViewInject(R.id.lv_list2)
    private ListView lvList;

    /**
     * GridView实例对象
     */
    @ViewInject(R.id.gv_list)
    private GridView gvList;

    /**
     * 存储照片新闻的集合
     */
    private ArrayList<PhotosBean.PhotoNews> mPhotoList;

    public PhotosMenuDetailPaper(Activity activity) {
        super(activity);
    }

    @Override
    public View initViews() {
        // 给空的帧布局动态添加布局对象
        /*
        TextView view = new TextView(mActivity);
        view.setTextSize(22);
        view.setTextColor(Color.RED);
        view.setGravity(Gravity.CENTER); // 居中显示
        view.setText("菜单详情页-组图");
         */
        View view = View.inflate(mActivity, R.layout.paper_photos_menu_detail,null);
        ViewUtils.inject(this,view);
        return view;
    }

    /**
     * 初始化数据
     */
    @Override
    public void initData() {
        String cache = CacheUtil.getCache(mActivity, GlobalConstans.PHOTOS_URL);
        if (!TextUtils.isEmpty(cache)){
            processData(cache);
        }
        getDataFromServer();
    }

    /**
     * 从服务器获取数据
     */
    private void getDataFromServer() {
        HttpUtils utils = new HttpUtils();
        utils.send(HttpRequest.HttpMethod.GET, GlobalConstans.PHOTOS_URL, new RequestCallBack<String>() {
            @Override
            public void onSuccess(ResponseInfo<String> responseInfo) {
                String result = responseInfo.result;
                processData(result);
                CacheUtil.setCache(mActivity,GlobalConstans.PHOTOS_URL,result);
            }

            @Override
            public void onFailure(HttpException e, String s) {

            }
        });
    }

    /**
     * 加载数据
     * @param result
     */
    private void processData(String result){
        Gson gson = new Gson();
        PhotosBean photosBean = gson.fromJson(result, PhotosBean.class);
        mPhotoList = photosBean.data.news;
    }
}

2.填充组图页面数据

将布局设置好后,接下来就是向布局中填充页面数据

  1. 修改PhotosMenuDetailPaper,添加内部类适配器,为ListView填充数据,代码如下:
package com.example.zhbj.base.impl.menudetail;

import android.app.Activity;
import android.text.TextUtils;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.GridView;
import android.widget.ListView;

import com.example.zhbj.R;
import com.example.zhbj.base.BaseMenuDetailPaper;
import com.example.zhbj.domain.PhotosBean;
import com.example.zhbj.global.GlobalConstans;
import com.example.zhbj.util.CacheUtil;
import com.google.gson.Gson;
import com.lidroid.xutils.HttpUtils;
import com.lidroid.xutils.ViewUtils;
import com.lidroid.xutils.exception.HttpException;
import com.lidroid.xutils.http.ResponseInfo;
import com.lidroid.xutils.http.callback.RequestCallBack;
import com.lidroid.xutils.http.client.HttpRequest;
import com.lidroid.xutils.view.annotation.ViewInject;

import java.util.ArrayList;

/**
 * 菜单详情页 - 组图
 */
public class PhotosMenuDetailPaper extends BaseMenuDetailPaper {

    /**
     * ListView实例对象
     */
    @ViewInject(R.id.lv_list2)
    private ListView lvList;

    /**
     * GridView实例对象
     */
    @ViewInject(R.id.gv_list)
    private GridView gvList;

    /**
     * 存储照片新闻的集合
     */
    private ArrayList<PhotosBean.PhotoNews> mPhotoList;

    public PhotosMenuDetailPaper(Activity activity) {
        super(activity);
    }

    @Override
    public View initViews() {
        // 给空的帧布局动态添加布局对象
        /*
        TextView view = new TextView(mActivity);
        view.setTextSize(22);
        view.setTextColor(Color.RED);
        view.setGravity(Gravity.CENTER); // 居中显示
        view.setText("菜单详情页-组图");
         */
        View view = View.inflate(mActivity, R.layout.paper_photos_menu_detail,null);
        ViewUtils.inject(this,view);
        return view;
    }

    /**
     * 初始化数据
     */
    @Override
    public void initData() {
        String cache = CacheUtil.getCache(mActivity, GlobalConstans.PHOTOS_URL);
        if (!TextUtils.isEmpty(cache)){
            processData(cache);
        }
        getDataFromServer();
    }

    /**
     * 从服务器获取数据
     */
    private void getDataFromServer() {
        HttpUtils utils = new HttpUtils();
        utils.send(HttpRequest.HttpMethod.GET, GlobalConstans.PHOTOS_URL, new RequestCallBack<String>() {
            @Override
            public void onSuccess(ResponseInfo<String> responseInfo) {
                String result = responseInfo.result;
                processData(result);
                CacheUtil.setCache(mActivity,GlobalConstans.PHOTOS_URL,result);
            }

            @Override
            public void onFailure(HttpException e, String s) {

            }
        });
    }

    /**
     * 加载数据
     * @param result
     */
    private void processData(String result){
        Gson gson = new Gson();
        PhotosBean photosBean = gson.fromJson(result, PhotosBean.class);
        mPhotoList = photosBean.data.news;
    }

    class PhotosAdapter extends BaseAdapter{

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

        @Override
        public PhotosBean.PhotoNews getItem(int position) {
            return mPhotoList.get(position);
        }

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

        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
            return null;
        }
    }
}
  1. 增加list_item_photo.xml布局文件,作为ListView的卡片式布局,代码如下:
<?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="wrap_content"
    android:orientation="vertical">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_margin="10dp"
        android:background="@drawable/pic_item_list_default"
        android:orientation="vertical">

        <ImageView
            android:id="@+id/iv_pic"
            android:layout_width="match_parent"
            android:layout_height="180dp"
            android:scaleType="centerCrop"
            android:src="@drawable/pic_item_list_default"/>

        <TextView
            android:id="@+id/tv_title4"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:padding="5dp"
            android:textColor="#000"
            android:textSize="18sp"
            android:text="标题"
            android:singleLine="true"/>

    </LinearLayout>

</LinearLayout>
  1. 修改PhotosMenuDetailPaper,重写getView()方法,并且再编写内部类ViewHolder,代码如下:
package com.example.zhbj.base.impl.menudetail;

import android.app.Activity;
import android.text.TextUtils;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.GridView;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.TextView;

import com.example.zhbj.R;
import com.example.zhbj.base.BaseMenuDetailPaper;
import com.example.zhbj.domain.PhotosBean;
import com.example.zhbj.global.GlobalConstans;
import com.example.zhbj.util.CacheUtil;
import com.google.gson.Gson;
import com.lidroid.xutils.BitmapUtils;
import com.lidroid.xutils.HttpUtils;
import com.lidroid.xutils.ViewUtils;
import com.lidroid.xutils.exception.HttpException;
import com.lidroid.xutils.http.ResponseInfo;
import com.lidroid.xutils.http.callback.RequestCallBack;
import com.lidroid.xutils.http.client.HttpRequest;
import com.lidroid.xutils.view.annotation.ViewInject;

import java.util.ArrayList;

/**
 * 菜单详情页 - 组图
 */
public class PhotosMenuDetailPaper extends BaseMenuDetailPaper {

    /**
     * ListView实例对象
     */
    @ViewInject(R.id.lv_list2)
    private ListView lvList;

    /**
     * GridView实例对象
     */
    @ViewInject(R.id.gv_list)
    private GridView gvList;

    /**
     * 存储照片新闻的集合
     */
    private ArrayList<PhotosBean.PhotoNews> mPhotoList;

    public PhotosMenuDetailPaper(Activity activity) {
        super(activity);
    }

    @Override
    public View initViews() {
        // 给空的帧布局动态添加布局对象
        /*
        TextView view = new TextView(mActivity);
        view.setTextSize(22);
        view.setTextColor(Color.RED);
        view.setGravity(Gravity.CENTER); // 居中显示
        view.setText("菜单详情页-组图");
         */
        View view = View.inflate(mActivity, R.layout.paper_photos_menu_detail,null);
        ViewUtils.inject(this,view);
        return view;
    }

    /**
     * 初始化数据
     */
    @Override
    public void initData() {
        String cache = CacheUtil.getCache(mActivity, GlobalConstans.PHOTOS_URL);
        if (!TextUtils.isEmpty(cache)){
            processData(cache);
        }
        getDataFromServer();
    }

    /**
     * 从服务器获取数据
     */
    private void getDataFromServer() {
        HttpUtils utils = new HttpUtils();
        utils.send(HttpRequest.HttpMethod.GET, GlobalConstans.PHOTOS_URL, new RequestCallBack<String>() {
            @Override
            public void onSuccess(ResponseInfo<String> responseInfo) {
                String result = responseInfo.result;
                processData(result);
                CacheUtil.setCache(mActivity,GlobalConstans.PHOTOS_URL,result);
            }

            @Override
            public void onFailure(HttpException e, String s) {

            }
        });
    }

    /**
     * 加载数据
     * @param result
     */
    private void processData(String result){
        Gson gson = new Gson();
        PhotosBean photosBean = gson.fromJson(result, PhotosBean.class);
        mPhotoList = photosBean.data.news;
        // 给ListView设置数据
        lvList.setAdapter(new PhotosAdapter());
    }

    class PhotosAdapter extends BaseAdapter{

        /**
         * BitmapUtils的对象实例
         */
        private BitmapUtils mBitmapUtils;

        public PhotosAdapter() {
            mBitmapUtils = new BitmapUtils(mActivity);
            mBitmapUtils.configDefaultLoadingImage(R.drawable.pic_item_list_default);
        }

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

        @Override
        public PhotosBean.PhotoNews getItem(int position) {
            return mPhotoList.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 = View.inflate(mActivity,R.layout.list_item_photo,null);
                holder = new ViewHolder();
                holder.tvTitle = (TextView) convertView.findViewById(R.id.tv_title4);
                holder.ivPic = (ImageView) convertView.findViewById(R.id.iv_pic);
                convertView.setTag(holder);
            }else {
                holder = (ViewHolder) convertView.getTag();
            }
            PhotosBean.PhotoNews item = getItem(position);
            holder.tvTitle.setText(item.title);
            mBitmapUtils.display(holder.ivPic,item.listimage);
            return convertView;
        }
    }

    static class ViewHolder{
        public TextView tvTitle;
        public ImageView ivPic;
    }
}
  1. 修改PhotosMenuDetailPaper,修改processData()方法,给GridView填充数据,代码如下:
    /**
     * 加载数据
     * @param result
     */
    private void processData(String result){
        Gson gson = new Gson();
        PhotosBean photosBean = gson.fromJson(result, PhotosBean.class);
        mPhotoList = photosBean.data.news;
        // 给ListView设置数据
        lvList.setAdapter(new PhotosAdapter());

        // 给GridView设置数据
        gvList.setAdapter(new PhotosAdapter());
    }

3.组图切换展示方式

我们已经把数据填充到ListView和GridView中了,接下来就需要编写逻辑来在这两个视图之间切换

  1. 修改title_bar.xml,在标题上添加一个图形按钮,代码如下:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="@drawable/title_red_bg">

    <TextView
        android:id="@+id/tv_title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerHorizontal="true"
        android:layout_centerVertical="true"
        android:textColor="#fff"
        android:textSize="25sp"
        android:text=""/>

    <ImageButton
        android:id="@+id/btn_menu"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentLeft="true"
        android:layout_centerVertical="true"
        android:background="@null"
        android:layout_marginLeft="10dp"
        android:src="@drawable/img_menu"/>

    <ImageButton
        android:id="@+id/btn_back"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentLeft="true"
        android:layout_centerVertical="true"
        android:background="@null"
        android:layout_marginLeft="10dp"
        android:visibility="gone"
        android:src="@drawable/back"/>

    <LinearLayout
        android:id="@+id/ll_control"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentRight="true"
        android:layout_marginRight="10dp"
        android:layout_centerVertical="true"
        android:gravity="center_vertical"
        android:visibility="gone"
        android:orientation="horizontal">
        <ImageButton
            android:id="@+id/btn_textsize"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:background="@null"
            android:layout_marginRight="10dp"
            android:src="@drawable/icon_textsize"/>

        <ImageButton
            android:id="@+id/btn_share"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:background="@null"
            android:src="@drawable/icon_share"/>

    </LinearLayout>
    
    <ImageButton
        android:id="@+id/btn_display"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentRight="true"
        android:layout_centerVertical="true"
        android:layout_marginRight="10dp"
        android:background="@null"
        android:src="@drawable/icon_pic_grid_type"
        android:visibility="gone"/>

</RelativeLayout>
  1. 修改BasePaper,获取标题栏上刚刚创建好的实例,代码如下:
package com.example.zhbj.base;

import android.app.Activity;
import android.view.View;
import android.widget.FrameLayout;
import android.widget.ImageButton;
import android.widget.TextView;

import com.example.zhbj.MainActivity;
import com.example.zhbj.R;
import com.jeremyfeinstein.slidingmenu.lib.SlidingMenu;

/**
 * 5个标签页的基类
 *
 * 共性:子类都有标题栏,所以可以直接在父类中加载布局页面
 */
public class BasePaper {

    /**
     * Activity对象
     */
    public Activity mActivity;

    /**
     * 标题的对象
     */
    public TextView tvTitle;

    /**
     * 标题按钮的对象
     */
    public ImageButton btnMenu;

    /**
     * 当前页面的根布局
     */
    public View mRootView;

    /**
     * 切换布局的对象
     */
    public ImageButton btnDisplay;

    /**
     * 内容的对象
     */
    public FrameLayout flContainer;

    public BasePaper(Activity activity) {
        mActivity = activity;
        // 在页面对象创建时,就初始化了布局
        mRootView = initView();
    }

    /**
     * 初始化布局
     */
    public View initView(){
        View view = View.inflate(mActivity, R.layout.base_paper, null);
        tvTitle = (TextView) view.findViewById(R.id.tv_title);
        btnMenu = (ImageButton) view.findViewById(R.id.btn_menu);
        flContainer = (FrameLayout) view.findViewById(R.id.fl_container);
        btnDisplay = (ImageButton) view.findViewById(R.id.btn_display);
        // 点击菜单按钮,控制侧边栏开关
        btnMenu.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                toggle();
            }
        });
        return view;
    }

    /**
     * 控制侧边栏的开关
     */
    private void toggle() {
        MainActivity mainUI = (MainActivity) mActivity;
        SlidingMenu slidingMenu = mainUI.getSlidingMenu();
        slidingMenu.toggle(); // 如果当前为开,则关;反之亦然
    }

    /**
     * 初始化数据
     */
    public void initData(){

    }
}
  1. 修改NewsCenterPaper,修改processData()方法,修改创建对象时的传参值,代码如下:
    /**
     * 解析从服务器获取的JSON数据
     */
    private void processData(String json) {
        Gson gson = new Gson();
        // 通过json和对象类,来生成一个对象
        newsMenu = gson.fromJson(json, NewsMenu.class);

        // 找到侧边栏对象
        MainActivity mainUI = (MainActivity) mActivity;
        LeftMenuFragment fragment = mainUI.getLeftMenuFragment();
        fragment.setMenuData(newsMenu.data);

        // 网络请求成功之后,初始化四个菜单详情页
        mPapers = new ArrayList<>();
        mPapers.add(new NewsMenuDetailPaper(mActivity,newsMenu.data.get(0).children)); // 通过构造方法传递数据
        mPapers.add(new TopicMenuDetailPaper(mActivity));
        mPapers.add(new PhotosMenuDetailPaper(mActivity,btnDisplay));
        mPapers.add(new InteractMenuDetailPaper(mActivity));

        // 设置新闻菜单详情页为默认页面
        setMenuDetailPager(0);
    }
  1. 修改PhotosMenuDetailPaper,修改构造方法,并且实现接口OnClickListener,代码如下:
package com.example.zhbj.base.impl.menudetail;

import android.app.Activity;
import android.text.TextUtils;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.GridView;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.TextView;

import com.example.zhbj.R;
import com.example.zhbj.base.BaseMenuDetailPaper;
import com.example.zhbj.domain.PhotosBean;
import com.example.zhbj.global.GlobalConstans;
import com.example.zhbj.util.CacheUtil;
import com.google.gson.Gson;
import com.lidroid.xutils.BitmapUtils;
import com.lidroid.xutils.HttpUtils;
import com.lidroid.xutils.ViewUtils;
import com.lidroid.xutils.exception.HttpException;
import com.lidroid.xutils.http.ResponseInfo;
import com.lidroid.xutils.http.callback.RequestCallBack;
import com.lidroid.xutils.http.client.HttpRequest;
import com.lidroid.xutils.view.annotation.ViewInject;

import java.util.ArrayList;

/**
 * 菜单详情页 - 组图
 */
public class PhotosMenuDetailPaper extends BaseMenuDetailPaper implements View.OnClickListener {

    /**
     * ListView实例对象
     */
    @ViewInject(R.id.lv_list2)
    private ListView lvList;

    /**
     * GridView实例对象
     */
    @ViewInject(R.id.gv_list)
    private GridView gvList;

    /**
     * 存储照片新闻的集合
     */
    private ArrayList<PhotosBean.PhotoNews> mPhotoList;

    public PhotosMenuDetailPaper(Activity mActivity, ImageButton btnDisplay) {
        super(mActivity);
        btnDisplay.setOnClickListener(this); // 设置切换按钮的监听
    }


    @Override
    public View initViews() {
        // 给空的帧布局动态添加布局对象
        /*
        TextView view = new TextView(mActivity);
        view.setTextSize(22);
        view.setTextColor(Color.RED);
        view.setGravity(Gravity.CENTER); // 居中显示
        view.setText("菜单详情页-组图");
         */
        View view = View.inflate(mActivity, R.layout.paper_photos_menu_detail,null);
        ViewUtils.inject(this,view);
        return view;
    }

    /**
     * 初始化数据
     */
    @Override
    public void initData() {
        String cache = CacheUtil.getCache(mActivity, GlobalConstans.PHOTOS_URL);
        if (!TextUtils.isEmpty(cache)){
            processData(cache);
        }
        getDataFromServer();
    }

    /**
     * 从服务器获取数据
     */
    private void getDataFromServer() {
        HttpUtils utils = new HttpUtils();
        utils.send(HttpRequest.HttpMethod.GET, GlobalConstans.PHOTOS_URL, new RequestCallBack<String>() {
            @Override
            public void onSuccess(ResponseInfo<String> responseInfo) {
                String result = responseInfo.result;
                processData(result);
                CacheUtil.setCache(mActivity,GlobalConstans.PHOTOS_URL,result);
            }

            @Override
            public void onFailure(HttpException e, String s) {

            }
        });
    }

    /**
     * 加载数据
     * @param result
     */
    private void processData(String result){
        Gson gson = new Gson();
        PhotosBean photosBean = gson.fromJson(result, PhotosBean.class);
        mPhotoList = photosBean.data.news;
        // 给ListView设置数据
        lvList.setAdapter(new PhotosAdapter());

        // 给GridView设置数据
        gvList.setAdapter(new PhotosAdapter());
    }

    @Override
    public void onClick(View v) {
        
    }

    class PhotosAdapter extends BaseAdapter{

        /**
         * BitmapUtils的对象实例
         */
        private BitmapUtils mBitmapUtils;

        public PhotosAdapter() {
            mBitmapUtils = new BitmapUtils(mActivity);
            mBitmapUtils.configDefaultLoadingImage(R.drawable.pic_item_list_default);
        }

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

        @Override
        public PhotosBean.PhotoNews getItem(int position) {
            return mPhotoList.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 = View.inflate(mActivity,R.layout.list_item_photo,null);
                holder = new ViewHolder();
                holder.tvTitle = (TextView) convertView.findViewById(R.id.tv_title4);
                holder.ivPic = (ImageView) convertView.findViewById(R.id.iv_pic);
                convertView.setTag(holder);
            }else {
                holder = (ViewHolder) convertView.getTag();
            }
            PhotosBean.PhotoNews item = getItem(position);
            holder.tvTitle.setText(item.title);
            mBitmapUtils.display(holder.ivPic,item.listimage);
            return convertView;
        }
    }

    static class ViewHolder{
        public TextView tvTitle;
        public ImageView ivPic;
    }
}
  1. 修改PhotosMenuDetailPaper,重写onClick()方法,实现按钮的方法逻辑,代码如下:
package com.example.zhbj.base.impl.menudetail;

import android.app.Activity;
import android.text.TextUtils;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.GridView;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.TextView;

import com.example.zhbj.R;
import com.example.zhbj.base.BaseMenuDetailPaper;
import com.example.zhbj.domain.PhotosBean;
import com.example.zhbj.global.GlobalConstans;
import com.example.zhbj.util.CacheUtil;
import com.google.gson.Gson;
import com.lidroid.xutils.BitmapUtils;
import com.lidroid.xutils.HttpUtils;
import com.lidroid.xutils.ViewUtils;
import com.lidroid.xutils.exception.HttpException;
import com.lidroid.xutils.http.ResponseInfo;
import com.lidroid.xutils.http.callback.RequestCallBack;
import com.lidroid.xutils.http.client.HttpRequest;
import com.lidroid.xutils.view.annotation.ViewInject;

import java.util.ArrayList;

/**
 * 菜单详情页 - 组图
 */
public class PhotosMenuDetailPaper extends BaseMenuDetailPaper implements View.OnClickListener {

    /**
     * ListView实例对象
     */
    @ViewInject(R.id.lv_list2)
    private ListView lvList;

    /**
     * GridView实例对象
     */
    @ViewInject(R.id.gv_list)
    private GridView gvList;

    /**
     * 存储照片新闻的集合
     */
    private ArrayList<PhotosBean.PhotoNews> mPhotoList;

    /**
     * 判断是否是ListView
     */
    private boolean isListView = true;

    private ImageButton btnDisplay;

    public PhotosMenuDetailPaper(Activity mActivity, ImageButton btnDisplay) {
        super(mActivity);
        this.btnDisplay = btnDisplay;
        btnDisplay.setOnClickListener(this); // 设置切换按钮的监听
    }


    @Override
    public View initViews() {
        // 给空的帧布局动态添加布局对象
        /*
        TextView view = new TextView(mActivity);
        view.setTextSize(22);
        view.setTextColor(Color.RED);
        view.setGravity(Gravity.CENTER); // 居中显示
        view.setText("菜单详情页-组图");
         */
        View view = View.inflate(mActivity, R.layout.paper_photos_menu_detail,null);
        ViewUtils.inject(this,view);
        return view;
    }

    /**
     * 初始化数据
     */
    @Override
    public void initData() {
        String cache = CacheUtil.getCache(mActivity, GlobalConstans.PHOTOS_URL);
        if (!TextUtils.isEmpty(cache)){
            processData(cache);
        }
        getDataFromServer();
    }

    /**
     * 从服务器获取数据
     */
    private void getDataFromServer() {
        HttpUtils utils = new HttpUtils();
        utils.send(HttpRequest.HttpMethod.GET, GlobalConstans.PHOTOS_URL, new RequestCallBack<String>() {
            @Override
            public void onSuccess(ResponseInfo<String> responseInfo) {
                String result = responseInfo.result;
                processData(result);
                CacheUtil.setCache(mActivity,GlobalConstans.PHOTOS_URL,result);
            }

            @Override
            public void onFailure(HttpException e, String s) {

            }
        });
    }

    /**
     * 加载数据
     * @param result
     */
    private void processData(String result){
        Gson gson = new Gson();
        PhotosBean photosBean = gson.fromJson(result, PhotosBean.class);
        mPhotoList = photosBean.data.news;
        // 给ListView设置数据
        lvList.setAdapter(new PhotosAdapter());

        // 给GridView设置数据
        gvList.setAdapter(new PhotosAdapter());
    }

    @Override
    public void onClick(View v) {
        if (isListView){
            // 显示GridView
            lvList.setVisibility(View.GONE);
            gvList.setVisibility(View.VISIBLE);
            btnDisplay.setImageResource(R.drawable.icon_pic_list_type);
            isListView = false;
        }else {
            // 显示ListView
            lvList.setVisibility(View.VISIBLE);
            gvList.setVisibility(View.GONE);
            btnDisplay.setImageResource(R.drawable.icon_pic_grid_type);
            isListView = true;
        }
    }

    class PhotosAdapter extends BaseAdapter{

        /**
         * BitmapUtils的对象实例
         */
        private BitmapUtils mBitmapUtils;

        public PhotosAdapter() {
            mBitmapUtils = new BitmapUtils(mActivity);
            mBitmapUtils.configDefaultLoadingImage(R.drawable.pic_item_list_default);
        }

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

        @Override
        public PhotosBean.PhotoNews getItem(int position) {
            return mPhotoList.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 = View.inflate(mActivity,R.layout.list_item_photo,null);
                holder = new ViewHolder();
                holder.tvTitle = (TextView) convertView.findViewById(R.id.tv_title4);
                holder.ivPic = (ImageView) convertView.findViewById(R.id.iv_pic);
                convertView.setTag(holder);
            }else {
                holder = (ViewHolder) convertView.getTag();
            }
            PhotosBean.PhotoNews item = getItem(position);
            holder.tvTitle.setText(item.title);
            mBitmapUtils.display(holder.ivPic,item.listimage);
            return convertView;
        }
    }

    static class ViewHolder{
        public TextView tvTitle;
        public ImageView ivPic;
    }
}
  1. 修改NewsCenterPaper,修改setMenuDetailPager()方法,代码如下:
	/**
     * 设置新闻中心的详情页
     * @param position
     */
    public void setMenuDetailPager(int position){
        BaseMenuDetailPaper paper = mPapers.get(position);

        // 判断是否是组图,如果是,显示切换按钮,否则隐藏
        if (paper instanceof PhotosMenuDetailPaper){
            btnDisplay.setVisibility(View.VISIBLE);
        }else {
            btnDisplay.setVisibility(View.GONE);
        }

        // 清除之前帧布局显示的内容
        flContainer.removeAllViews();

        // 修改当前帧布局显示的内容
        flContainer.addView(paper.mRootView);

        // 初始化当前页面的数据
        paper.initData();

        // 修改标题栏
        tvTitle.setText(newsMenu.data.get(position).title);
    }

4.三级缓存原理

到这里,其实整个项目已经开发完毕了。但是为了更进一步,我们需要学习更多关于应用优化的内容,这就是这篇要学习的内容:三级缓存

按照先后顺序,三级缓存分别指的是:

  1. 内存缓存

    速度很快,不浪费流量,优先

  2. 本地缓存(SDCard)

    速度快,不浪费流量,其次

  3. 网络缓存

    速度慢,浪费流量,最后

5.网络缓存开发

前面我们提到了三级缓存的原理,现在就来动手实现一下

  1. 由于xUtils中的BitmapUtils已经实现了网络缓存,我们这里也来写一个类似的工具类。在util包下新建一个bitmap包,表示图片工具类,再新建一个MyBitmapUtils,表示实现了网络缓存的图片工具类,代码如下:
package com.example.zhbj.util.bitmap;

import android.widget.ImageView;

/**
 * 自定义三级缓存工具类
 */
public class MyBitmapUtils {

    /**
     * 加载图片进行展示
     * @param imageView ImageView组件
     * @param url
     */
    public void display(ImageView imageView,String url){
        // 一级缓存:内存缓存

        // 二级缓存:本地缓存

        // 三级缓存:网络缓存
    }
}
  1. 在bitmap包下新建NetCacheUtil,表示网络缓存的工具类,实现相应逻辑,代码如下:
package com.example.zhbj.util.bitmap;

import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.AsyncTask;
import android.widget.ImageView;

import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;

/**
 * 网络缓存工具类
 */
public class NetCacheUtils {

    /**
     * ImaegView对象
     */
    private ImageView mImageView;

    /**
     * 图片url
     */
    private String url;

    /**
     * 异步下载图片
     * @param imageView ImageView组件
     * @param url uri地址
     */
    public void getBitmapFromNet(ImageView imageView, String url) {
        // 调用AsyncTask进行异步下载
        new BitmapTask().execute(imageView,url);
    }

    class BitmapTask extends AsyncTask<Object,Void,Bitmap>{

        @Override
        protected void onPreExecute() {
            super.onPreExecute();
        }

        @Override
        protected Bitmap doInBackground(Object... params) {
            mImageView = (ImageView) params[0];
            url = (String) params[1];
            // 给当前ImageView打上标签
            mImageView.setTag(url);
            // 使用url下载图片
            Bitmap bitmap = download(url);
            return bitmap;
        }

        @Override
        protected void onPostExecute(Bitmap bitmap) {
            // 将下载好的图片设置给ImaegView
            /*
             注意:由于ListView的重用机制,导致某个item有可能展示它所重用的那个item的图片,导致图片错乱(即数据不同步)
             解决方案:确保当前设置的图片和当前显示的ImageView完全匹配
             */
            String tag = (String) mImageView.getTag(); // 获取和当前ImageView绑定的url
            if (url.equals(tag)){ // 判断当前下载的图片url是否和ImageView中的url一致,如果一致,说明图片正确
                mImageView.setImageBitmap(bitmap);
            }
        }
    }

    /**
     * 使用url下载图片
     * @param url url地址
     * @return
     */
    public Bitmap download(String url) {
        // 创建连接对象
        HttpURLConnection conn = null;
        try {
            conn = (HttpURLConnection) new URL(url).openConnection();
            conn.setRequestMethod("GET");
            conn.setConnectTimeout(6000);
            conn.setReadTimeout(6000);
            conn.connect();
            int responseCode = conn.getResponseCode();
            if (responseCode == 200){
                InputStream in = conn.getInputStream();
                // 使用输入流生成一个Bitmap对象
                Bitmap bitmap = BitmapFactory.decodeStream(in);
                return bitmap;
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (conn != null){
                conn.disconnect();
            }
        }
        return null;
    }
}
  1. 修改MyBitmapUtils,调用NetCacheUtil的getBitmapFromNet()方法,代码如下:
package com.example.zhbj.util.bitmap;

import android.widget.ImageView;

/**
 * 自定义三级缓存工具类
 */
public class MyBitmapUtils {

    /**
     * 网络缓存工具类对象
     */
    private NetCacheUtils mNetCacheUtils;

    public MyBitmapUtils() {
        mNetCacheUtils = new NetCacheUtils();
    }

    /**
     * 加载图片进行展示
     * @param imageView ImageView组件
     * @param url uri地址
     */
    public void display(ImageView imageView,String url){
        // 一级缓存:内存缓存

        // 二级缓存:本地缓存

        // 三级缓存:网络缓存
        mNetCacheUtils.getBitmapFromNet(imageView,url);
    }
}

6.本地缓存开发

之前开发完网络缓存后,会发现每当要从网上获取数据时都要下载数据,这将十分不便,所以现在来开发本地缓存

  1. 这里要使用MD5的一个工具类,MD5Encoder,将它放在util包下,代码如下:
package com.example.zhbj.util;

import java.security.MessageDigest;

public class MD5Encoder {
	
	public static String encode(String string) throws Exception {
	    byte[] hash = MessageDigest.getInstance("MD5").digest(string.getBytes("UTF-8"));
	    StringBuilder hex = new StringBuilder(hash.length * 2);
	    for (byte b : hash) {
	        if ((b & 0xFF) < 0x10) {
	        	hex.append("0");
	        }
	        hex.append(Integer.toHexString(b & 0xFF));
	    }
	    return hex.toString();
	}
}
  1. 在util包下新建LocalCacheUtils,表示本地缓存的工具类,并且完善相应逻辑,代码如下:
package com.example.zhbj.util.bitmap;

import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Environment;

import com.example.zhbj.util.MD5Encoder;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;

/**
 * 本地缓存工具类
 */
public class LocalCacheUtils {

    /**
     * 缓存文件夹的路径
     */
    private String PATH = Environment.getExternalStorageDirectory().getAbsolutePath() + "/zhbj_cache/";;

    /**
     * 写缓存
     * @param url url地址
     * @param bitmap 图片
     */
    public void setLocalCache(String url,Bitmap bitmap){
        // 将图片保存在本地文件
        File dir = new File(PATH);
        if (!dir.exists() || !dir.isDirectory()){
            dir.mkdirs(); // 创建文件夹
        }
        try {
            File cacheFile = new File(dir, MD5Encoder.encode(url)); // 创建本地的文件,以url的md5命名
            // 将图片压缩保存在本地;参数1:图片格式,参数2:压缩比(0-100,100表示不压缩),参数3:输出流
            bitmap.compress(Bitmap.CompressFormat.JPEG,100,new FileOutputStream(cacheFile));
        }catch (Exception e){
            e.printStackTrace();
        }
    }

    /**
     * 读缓存
     * @param url url地址
     */
    public Bitmap getLocalCache(String url){
        try {
            File cacheFile = new File(url,MD5Encoder.encode(url)); // 创建本地的文件,以url的md5命名
            if (cacheFile.exists()){
                // 缓存存在
                Bitmap bitmap = BitmapFactory.decodeStream(new FileInputStream(cacheFile));
                return bitmap;
            }
        }catch (Exception e){
        }
        return null;
    }
}
  1. 修改MyBitmapUtils,调用LocalCacheUtils的getLocalCache()方法,代码如下:
package com.example.zhbj.util.bitmap;

import android.graphics.Bitmap;
import android.widget.ImageView;

/**
 * 自定义三级缓存工具类
 */
public class MyBitmapUtils {

    /**
     * 本地缓存工具类对象
     */
    private LocalCacheUtils mLocalCacheUtils;

    /**
     * 网络缓存工具类对象
     */
    private NetCacheUtils mNetCacheUtils;

    public MyBitmapUtils() {
        mLocalCacheUtils = new LocalCacheUtils();
        mNetCacheUtils = new NetCacheUtils(mLocalCacheUtils);
    }

    /**
     * 加载图片进行展示
     * @param imageView ImageView组件
     * @param url uri地址
     */
    public void display(ImageView imageView,String url){
        // 一级缓存:内存缓存

        // 二级缓存:本地缓存
        Bitmap bitmap = mLocalCacheUtils.getLocalCache(url);
        if (bitmap != null){
            imageView.setImageBitmap(bitmap);
            return;
        }

        // 三级缓存:网络缓存
        mNetCacheUtils.getBitmapFromNet(imageView,url);
    }
}
  1. 修改NetCacheUtils,在从网上获取数据时将缓存写入文件中,代码如下:
package com.example.zhbj.util.bitmap;

import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.AsyncTask;
import android.widget.ImageView;

import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;

/**
 * 网络缓存工具类
 */
public class NetCacheUtils {

    /**
     * ImaegView对象
     */
    private ImageView mImageView;

    /**
     * 图片url
     */
    private String url;

    /**
     * 本地缓存的工具类对象
     */
    private LocalCacheUtils mLocalCacheUtils;

    public NetCacheUtils(LocalCacheUtils localCacheUtils) {
        mLocalCacheUtils = localCacheUtils;
    }

    /**
     * 异步下载图片
     * @param imageView ImageView组件
     * @param url uri地址
     */
    public void getBitmapFromNet(ImageView imageView, String url) {
        // 调用AsyncTask进行异步下载
        new BitmapTask().execute(imageView,url);
    }

    class BitmapTask extends AsyncTask<Object,Void,Bitmap>{

        @Override
        protected void onPreExecute() {
            super.onPreExecute();
        }

        @Override
        protected Bitmap doInBackground(Object... params) {
            mImageView = (ImageView) params[0];
            url = (String) params[1];
            // 给当前ImageView打上标签
            mImageView.setTag(url);
            // 使用url下载图片
            Bitmap bitmap = download(url);
            return bitmap;
        }

        @Override
        protected void onPostExecute(Bitmap bitmap) {
            // 将下载好的图片设置给ImaegView
            /*
             注意:由于ListView的重用机制,导致某个item有可能展示它所重用的那个item的图片,导致图片错乱(即数据不同步)
             解决方案:确保当前设置的图片和当前显示的ImageView完全匹配
             */
            if (bitmap != null){
                String tag = (String) mImageView.getTag(); // 获取和当前ImageView绑定的url
                if (url.equals(tag)){ // 判断当前下载的图片url是否和ImageView中的url一致,如果一致,说明图片正确
                    mImageView.setImageBitmap(bitmap);

                    // 写本地缓存
                    mLocalCacheUtils.setLocalCache(url,bitmap);
                }
            }
        }
    }

    /**
     * 使用url下载图片
     * @param url url地址
     * @return
     */
    public Bitmap download(String url) {
        // 创建连接对象
        HttpURLConnection conn = null;
        try {
            conn = (HttpURLConnection) new URL(url).openConnection();
            conn.setRequestMethod("GET");
            conn.setConnectTimeout(6000);
            conn.setReadTimeout(6000);
            conn.connect();
            int responseCode = conn.getResponseCode();
            if (responseCode == 200){
                InputStream in = conn.getInputStream();
                // 使用输入流生成一个Bitmap对象
                Bitmap bitmap = BitmapFactory.decodeStream(in);
                return bitmap;
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (conn != null){
                conn.disconnect();
            }
        }
        return null;
    }
}

7.内存缓存开发

之前开发完本地缓存后,现在再来开发内存缓存

  1. 在bitmap包下新建MemoryCacheUtils,表示内存缓存的工具类,并且完善相应逻辑,代码如下:
package com.example.zhbj.util.bitmap;

import android.graphics.Bitmap;

import java.util.HashMap;

/**
 * 内存缓存工具类
 */
public class MemoryCacheUtils {

    /**
     * 存储内存空间的map对象
     */
    private HashMap<String, Bitmap> mHashMap = new HashMap<>();

    /**
     * 写缓存
     * @param url url地址
     * @param bitmap bitmap对象
     */
    public void setMemoryCache(String url,Bitmap bitmap){
        mHashMap.put(url,bitmap);
    }

    /**
     * 读缓存
     * @param url url地址
     * @return
     */
    public Bitmap getMemoryCache(String url){
        return mHashMap.get(url);
    }
}
  1. 修改MyBitmapUtils,调用MemoryCacheUtils的getMemoryCache()方法,代码如下:
package com.example.zhbj.util.bitmap;

import android.graphics.Bitmap;
import android.widget.ImageView;

/**
 * 自定义三级缓存工具类
 */
public class MyBitmapUtils {

    /**
     * 内存缓存工具类对象
     */
    private MemoryCacheUtils mMemoryCacheUtils;

    /**
     * 本地缓存工具类对象
     */
    private LocalCacheUtils mLocalCacheUtils;

    /**
     * 网络缓存工具类对象
     */
    private NetCacheUtils mNetCacheUtils;

    public MyBitmapUtils() {
        mMemoryCacheUtils = new MemoryCacheUtils();
        mLocalCacheUtils = new LocalCacheUtils();
        mNetCacheUtils = new NetCacheUtils(mLocalCacheUtils,mMemoryCacheUtils);
    }

    /**
     * 加载图片进行展示
     * @param imageView ImageView组件
     * @param url uri地址
     */
    public void display(ImageView imageView,String url){
        // 一级缓存:内存缓存
        Bitmap bitmap = mMemoryCacheUtils.getMemoryCache(url);
        if (bitmap != null){
            imageView.setImageBitmap(bitmap);
            return;
        }

        // 二级缓存:本地缓存
        bitmap = mLocalCacheUtils.getLocalCache(url);
        if (bitmap != null){
            imageView.setImageBitmap(bitmap);
            // 写内存缓存
            mMemoryCacheUtils.setMemoryCache(url,bitmap);
            return;
        }

        // 三级缓存:网络缓存
        mNetCacheUtils.getBitmapFromNet(imageView,url);
    }
}
  1. 修改NetCacheUtils,在从网上获取数据时将缓存写入内存中,代码如下:
package com.example.zhbj.util.bitmap;

import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.AsyncTask;
import android.widget.ImageView;

import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;

/**
 * 网络缓存工具类
 */
public class NetCacheUtils {

    /**
     * ImaegView对象
     */
    private ImageView mImageView;

    /**
     * 图片url
     */
    private String url;

    /**
     * 内存缓存的工具类对象
     */
    private MemoryCacheUtils mMemoryCacheUtils;

    /**
     * 本地缓存的工具类对象
     */
    private LocalCacheUtils mLocalCacheUtils;

    public NetCacheUtils(LocalCacheUtils localCacheUtils, MemoryCacheUtils memoryCacheUtils) {
        mLocalCacheUtils = localCacheUtils;
        mMemoryCacheUtils = memoryCacheUtils;
    }

    /**
     * 异步下载图片
     * @param imageView ImageView组件
     * @param url uri地址
     */
    public void getBitmapFromNet(ImageView imageView, String url) {
        // 调用AsyncTask进行异步下载
        new BitmapTask().execute(imageView,url);
    }

    class BitmapTask extends AsyncTask<Object,Void,Bitmap>{

        @Override
        protected void onPreExecute() {
            super.onPreExecute();
        }

        @Override
        protected Bitmap doInBackground(Object... params) {
            mImageView = (ImageView) params[0];
            url = (String) params[1];
            // 给当前ImageView打上标签
            mImageView.setTag(url);
            // 使用url下载图片
            Bitmap bitmap = download(url);
            return bitmap;
        }

        @Override
        protected void onPostExecute(Bitmap bitmap) {
            // 将下载好的图片设置给ImaegView
            /*
             注意:由于ListView的重用机制,导致某个item有可能展示它所重用的那个item的图片,导致图片错乱(即数据不同步)
             解决方案:确保当前设置的图片和当前显示的ImageView完全匹配
             */
            if (bitmap != null){
                String tag = (String) mImageView.getTag(); // 获取和当前ImageView绑定的url
                if (url.equals(tag)){ // 判断当前下载的图片url是否和ImageView中的url一致,如果一致,说明图片正确
                    mImageView.setImageBitmap(bitmap);

                    // 写本地缓存
                    mLocalCacheUtils.setLocalCache(url,bitmap);

                    // 写内存缓存
                    mMemoryCacheUtils.setMemoryCache(url,bitmap);
                }
            }
        }
    }

    /**
     * 使用url下载图片
     * @param url url地址
     * @return
     */
    public Bitmap download(String url) {
        // 创建连接对象
        HttpURLConnection conn = null;
        try {
            conn = (HttpURLConnection) new URL(url).openConnection();
            conn.setRequestMethod("GET");
            conn.setConnectTimeout(6000);
            conn.setReadTimeout(6000);
            conn.connect();
            int responseCode = conn.getResponseCode();
            if (responseCode == 200){
                InputStream in = conn.getInputStream();
                // 使用输入流生成一个Bitmap对象
                Bitmap bitmap = BitmapFactory.decodeStream(in);
                return bitmap;
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (conn != null){
                conn.disconnect();
            }
        }
        return null;
    }
}

8.内存溢出原理

现在完成了三级缓存的开发,接下来就要将目光着重放在一级内存:内存缓存中,因为内存缓存可能会发生内存溢出的问题

先来简单介绍两个概念:

假设现在有一行语句:Person p = new Person()执行后在内存中存放如下:

  • :存放p(引用)
  • :存放Person(对象)

在Davik虚拟机中,默认给每个app分配16M内存空间,和手机配置无关

内存溢出指的是:里的对象过多,超过了16M的内存空间,导致了内存溢出;根本原因是虚拟机挂掉了

Java中具有GC(垃圾回收器)机制,会自动回收没有引用的对象。但是这里编写的内存缓存工具类中引用的是HashMap,存放的都是有引用的对象,因此不会被这个机制影响

9.软应用介绍

介绍完内存溢出后,现在按照引用强度顺序,简单介绍一下Java中的引用体系:

  • 强引用(默认):当内存不够时,垃圾回收器也不会考虑回收
  • 软引用:当内存不够时,垃圾回收器会考虑回收,对象类为SoftReference
  • 弱引用:当内存不够时,垃圾回收器更会考虑回收
  • 虚引用:当内存不够时,垃圾回收器最会考虑回收,对象类为PhantomReference

接下来,围绕软引用对MemoryCacheUtils进行改造,代码如下:

package com.example.zhbj.util.bitmap;

import android.graphics.Bitmap;

import java.lang.ref.SoftReference;
import java.util.HashMap;

/**
 * 内存缓存工具类
 */
public class MemoryCacheUtils {

    /**
     * 存储内存空间的map对象
     */
    private HashMap<String, SoftReference<Bitmap>> mHashMap = new HashMap<>();

    /**
     * 写缓存
     * @param url url地址
     * @param bitmap bitmap对象
     */
    public void setMemoryCache(String url,Bitmap bitmap){
        SoftReference<Bitmap> soft = new SoftReference<Bitmap>(bitmap); // 用软引用包装btmap
        mHashMap.put(url,soft);
    }

    /**
     * 读缓存
     * @param url url地址
     * @return
     */
    public Bitmap getMemoryCache(String url){
        SoftReference<Bitmap> soft = mHashMap.get(url);
        if (soft != null){
            Bitmap bitmap = soft.get(); // 从软引用取出当前对象
            return bitmap;
        }
        return null;
    }
}

10.LruCache的使用

之前我们编写了运用软应用的内存缓存工具类,但是并没有派上作用。

翻阅文档之后,是因为:

因为从 Android 2.3 (API Level 9)开始,垃圾回收器会更倾向于回收持有软引用或弱引用的对象,这让软引用和弱引用变得不再可靠。

所以,我们需要找到SoftReference的替代品,也就是LruCache,对MemoryCacheUtils进行改造,代码如下:

package com.example.zhbj.util.bitmap;

import android.graphics.Bitmap;
import android.support.annotation.NonNull;
import android.support.v4.util.LruCache;

import java.lang.ref.SoftReference;
import java.util.HashMap;

/**
 * 内存缓存工具类
 */
public class MemoryCacheUtils {

    /**
     * 存储内存空间的map对象
     */
    private HashMap<String, SoftReference<Bitmap>> mHashMap = new HashMap<>();

    /**
     * LruCache对象实例
     */
    private LruCache<String,Bitmap> mLrucache;

    public MemoryCacheUtils() {
        long maxMemory = Runtime.getRuntime().maxMemory(); // 获取虚拟机分配的最大内存,默认为16mb
        // 需要传入一个内存缓存上限
        mLrucache = new LruCache<String,Bitmap>((int) (maxMemory / 8)){

            /**
             * 返回单个对象占用内存的大小
             * @param key
             * @param value
             * @return
             */
            @Override
            protected int sizeOf(@NonNull String key, @NonNull Bitmap value) {
                // 计算图片占用的内存大小
                int byteCount = value.getByteCount();
                return byteCount;
            }
        };
    }

    /**
     * 写缓存
     * @param url url地址
     * @param bitmap bitmap对象
     */
    public void setMemoryCache(String url,Bitmap bitmap){
        /*
        SoftReference<Bitmap> soft = new SoftReference<Bitmap>(bitmap); // 用软引用包装btmap
        mHashMap.put(url,soft);
         */
        mLrucache.put(url,bitmap);
    }

    /**
     * 读缓存
     * @param url url地址
     * @return
     */
    public Bitmap getMemoryCache(String url){
        /*
        SoftReference<Bitmap> soft = mHashMap.get(url);
        if (soft != null){
            Bitmap bitmap = soft.get(); // 从软引用取出当前对象
            return bitmap;
        }
        return null;
         */
        return mLrucache.get(url);
    }
}

11.LruCache原理分析

LRU:Least Recentlly Used,最近最少使用算法,LruCache就是对HashMap的封装

详细的原理,可以参考网上,这里不再详述

12.三级缓存总结

80%的内存溢出问题,都来源于图片加载,而解决内存溢出时,可以采用三级缓存策略

  • 网络缓存:AsyncTask

  • 本地缓存:File + MD5

  • 内存缓存:LruCache(替换掉SoftReference)

另外,内存溢出内存泄漏是有区别的,这点请注意

13.屏幕适配

在处理好三级缓存后,接下来就要进行项目的第二项优化:屏幕适配

屏幕适配有五大适配方式:

  • 图片适配
  • 布局适配
  • 尺寸适配
  • 权重适配
  • 代码适配

接下来分别介绍

13.1 图片适配

为了演示图片适配的效果,可以新建一个项目作为演示,随意在drawable中添加一张名为test.jpg的图片,然后启动分辨率不同的虚拟机,可以看到图片显示的差距

观察res目录下的drawable目录,可以看到有5个名字相似的目录,如图所示:

在这里插入图片描述

当然高版本的话会变成mipmap,如图所示:

在这里插入图片描述

在这几个文件夹中,都存放了不同分辨率的同一张图片ic_launcher.png和这张图片的圆角形式ic_launcher_round.png,如图所示:

在这里插入图片描述

其中,这五个等级的图片在启动应用后会根据相应的分辨率来播放图片,具体如下:

  • ipdi:240 x 320

  • mpdi:320 x 480

  • hpdi:480 x 800

  • xhdpi:1280 x 720

  • xxhdpi:1920 x 1080

系统会根据当前屏幕分辨率优先加载对应dpi目录的图片吗,如果匹配到该目录的图片文件不存在,会去查找相近目录的同名图片文件

在实际开发中,美工做一套图就可以了,一般基于主流分辨率(1280 * 720)即可。

13.2 布局适配

上面介绍了图片适配,现在来介绍布局适配

跟图片适配相似的,布局也可以写多个同名布局来适配不同分辨率的手机

使用方法就是写一个指定格式的layout文件夹,如图所示:

在这里插入图片描述

480 x 800,指的是专门适配800x480的屏幕

13.3 尺寸适配(重点)

在布局文件中,若是指定了控件的具体形状属性,在不同分辨率的手机上显示的效果也会相应地进行适配,造成这一点的主要原因是长度单位dp

每个设备都有一个设备密度,可以通过getResources().getDisplayMetrics().density这个api进行获取,各个不同分辨率的设备密度如下:

  • 240 * 320:0.75,ldpi

  • 320 * 480:1.0,mdpi

  • 480 * 800:1.5,hdpi

  • 1280 * 720:2,xhdpi

而长度单位dppx(像素)的关系如下:
d p = p x / dp = px / 设备密度
针对尺寸适配,在res/values下有个专门的文件dimens.xml来处理尺寸适配的问题

跟布局适配同样的,也可以创建一个指定格式的values文件夹,如图所示:

在这里插入图片描述

在该目录下新建一个dimens.xml,作用与values/dimens.xml相同

一般来说代码都是需要填入px的,而这里可以将dp切换成px,写一个工具类即可,代码如下:

package com.example.zhbj.util;

import android.content.Context;

/**
 *  长度单位转换的工具类
 */
public class DensityUtil {

    /**
     * 将dp转换成px
     * @param dp
     * @param context
     */
    public static int dpTopx(float dp, Context context){
        float density = context.getResources().getDisplayMetrics().density;
        int px = (int) (dp * density + 0.5f);
        return px;
    }

    /**
     * 将px转换成dp
     * @param px
     * @param context
     */
    public static float pxTodp(int px, Context context){
        float density = context.getResources().getDisplayMetrics().density;
        float dp = px / density;
        return dp;
    }
}

13.4 权重适配

上面介绍了图片适配,现在来介绍权重适配

在布局中,可以调用布局(组件)的这个属性:

android:weightSum=""

来指定组件在这个布局中的占用大小和位置

能用权重适配则尽量使用权重适配,但缺点是必须用线性布局

13.5 代码适配

上面介绍了权重适配,现在来介绍代码适配

跟权重适配类似,即在代码中获取到屏幕的宽高,即:

WindowManager wm = (WindowManager)getSystemService(WINDOW_SERVICE);
int width = wm.getDefaultDisplay().getWidth(); // 获取宽度
int height = wm.getDefaultDisplay().getHeight(); // 获取高度

通过WindowManager获取屏幕的宽高,然后给组件动态设置属性:

TextView textView = (TextView)findViewById(R.id.textview); // 获取控件实例
LinearLayout.LayoutParams params = (LayoutParams)textView.getLayoutParams();// 获取控件的参数值
params.width = width / 3; // 设置控件的宽度
params.height = height / 5; // 设置控件的高度
textView.setLayoutParams(params); // 重置控件的属性

13.6 总结

基于主流屏幕(1280 * 720)开发,开发后期,才会开始考虑屏幕适配

养成良好的开发习惯:

  • 多用线性布局相对布局
  • 少用绝对布局
  • 多用dp,不用px,如果一定需要使用px,可以将dp转化成px使用
发布了256 篇原创文章 · 获赞 53 · 访问量 3万+

猜你喜欢

转载自blog.csdn.net/qq_41151659/article/details/104119266
今日推荐