今天關於是寫ListView還是RecyclerView著實糾結了一會,畢竟這二者都很重要,不過最近做的東西里用的是RecyclerView,所以還是先寫RecyclerView了,而且程式碼也擼好了。好了日常嘮嗑(1/1),下面進入正題。
本文包含以下幾個方面:
- 使用RecyclerView實現ListView的效果
- RecyclerView載入圖片出現OOM時的一些處理
- RecyclerView實現瀑布流
- RecyclerView滑動卡頓的一些優化
RecyclerView與ListView原理是類似的:都是僅僅維護少量的View並且可以展示大量的資料,不過RecyclerView相比ListView更加高階和靈活。RecyclerView本身只負責View的回收和複用,這從它名字本身也能看出一些端倪。既然RecyclerView只關心View的回收和複用,那麼這玩意到底該怎麼才能玩起來?
好問題,雖然是我自己問的……借用一句別人說的:真正牛逼的人是不需要什麼都管的,只要底下有人就行。LayoutManager負責Item佈局和展示,Item之間的間隔由ItemDecoration來解決,Item的增加與刪除的動畫由ItemAnimator來解決,Adapter用來將資料與Item介面繫結。
好了作了基本的瞭解,開始上碼吧,有關如何在Android Studio中使用RecyclerView和我的程式碼獲取方法,將在文末作相應的說明。首先第一要實現的,就是和ListView類似的效果,先看效果圖,原諒我不會做動圖……
好了,看完效果,開碼,首先是主介面的xml檔案
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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.luo_pc.testforrecyclerview.MainActivity">
<android.support.v7.widget.RecyclerView
android:id="@+id/rv_test"
android:layout_width="match_parent"
android:layout_height="wrap_content"></android.support.v7.widget.RecyclerView>
</LinearLayout>
複製程式碼
沒啥玩意,就是一個線性佈局里加一個RecyclerView,接下來與ListView一樣,列表item的xml檔案
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="#ffffff"
>
<ImageView
android:id="@+id/iv_item_img"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"
android:adjustViewBounds="true"
android:layout_gravity="center"
/>
<TextView
android:id="@+id/tv_item_desc"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:textColor="#000000"
/>
</LinearLayout>
複製程式碼
主介面程式碼:
public class SecondActivity extends AppCompatActivity{
private ArrayList<ItemBean> itemList;
private RecyclerView rv_test;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
rv_test = (RecyclerView) findViewById(R.id.rv_test);
initData();
//設定線性layoutManager
rv_test.setLayoutManager(new LinearLayoutManager(getApplicationContext()));
mAdapter myAdapter = new mAdapter();
//設定myAdapter的資料
myAdapter.setData(itemList);
//設定Adapter
rv_test.setAdapter(myAdapter);
}
private void initData(){
itemList = new ArrayList<ItemBean>();
itemList.add(new ItemBean(R.drawable.bird, "南小鳥1"));
itemList.add(new ItemBean(R.drawable.xiyangyang, "喜羊羊"));
itemList.add(new ItemBean(R.drawable.bird3, "南小鳥2"));
itemList.add(new ItemBean(R.drawable.blackcat, "黑貓"));
itemList.add(new ItemBean(R.drawable.dog, "狗與剪刀"));
itemList.add(new ItemBean(R.drawable.nike3, "妮可妮可"));
itemList.add(new ItemBean(R.drawable.paojie, "炮姐"));
itemList.add(new ItemBean(R.drawable.saber1, "吾王1"));
itemList.add(new ItemBean(R.drawable.saber2, "吾王2"));
}
}
複製程式碼
上面的ItemBean程式碼:
public class ItemBean {
//R.drawable....
private int img;
private String desc;
public ItemBean(int img,String desc){
this.img = img;
this.desc = desc;
}
public int getImg(){
return img;
}
public void setImg(int img) {
this.img = img;
}
public String getDesc() {
return desc;
}
public void setDesc(String desc) {
this.desc = desc;
}
}
複製程式碼
接下來就是重頭戲了,Adapter的程式碼。我們需要建立一個類繼承RecyclerView.Adapter,以此來建立RecyclerView的介面卡,繼承RecyclerView.Adapter需要實現 onCreateViewHolder()、onBindViewHolder()和getItemCount()方法,而onCreateView的返回值是RecyclerView.ViewHolder,所以我們需要建立一個RecyclerView.ViewHolder。而RecyclerView.ViewHolder是一個抽象類,所以我們必須新建一個類繼承該類,該抽象類中的建構函式也必須實現。說了這麼多,可能有點繞,讓我們從程式碼中學習如何操作。
class mAdapter extends RecyclerView.Adapter{
//資料
private ArrayList<ItemBean> itemList;
// private ImageResizer imgResizer = new ImageResizer();
//設定資料
private void setData(ArrayList<ItemBean> itemList){
this.itemList = itemList;
}
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
//得到item的view
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.layout_item, parent, false);
//返回viewholder
return new mViewHolder(view);
}
//繫結資料
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
ItemBean it = itemList.get(position);
mViewHolder mholder = (mViewHolder)holder;
//設定imageView的圖片
mholder.iv_item_img.setImageResource(it.getImg());
// mholder.iv_item_img.setImageBitmap(imgResizer.decodeSampledBitmapFromResource(getResources(),it.getImg()));
//設定textView的文字
mholder.tv_item_desc.setText(it.getDesc());
}
@Override
public int getItemCount() {
return itemList.size();
}
class mViewHolder extends RecyclerView.ViewHolder{
ImageView iv_item_img;
TextView tv_item_desc;
public mViewHolder(View itemView) {
super(itemView);
iv_item_img = (ImageView) itemView.findViewById(R.id.iv_item_img);
tv_item_desc = (TextView)itemView.findViewById(R.id.tv_item_desc);
}
}
}
複製程式碼
我這有些地方寫的不是很好,在寫一個類繼承RecyclerView.Holder時這麼寫會更好
class mAdapter extends RecyclerView<mViewHolder>{}
複製程式碼
如此在onBindViewHolder方法中便可以直接使用holder了,而不必再強轉。
好了程式碼也寫完了,是時候跑一波了!我的圖片都是在百度上找的,隨便下了那麼幾張。跑的時候果然不負我望,OOM!
誒?OOM?那麼我之前放的效果圖是怎麼回事?沒錯……我壓縮了一下圖片,才跑出來開頭放的效果圖……關於該如何操作,容我細細道來。
我從網上下的圖都是比較大的,大多為jpg格式,用個看圖軟體開啟顯示是1125*1674畫素在windows系統上是436K大小。對於jpg我不太瞭解,只知道這換算成ARGB_8888每個畫素佔4byte,得7M了。而且
反正是OOM了,容我說一下我的處理方法:
壓縮
圖片有不同的形狀與大小。在大多數情況下它們的實際大小都比需要呈現的尺寸大很多。考慮到在有限的記憶體下工作,理想情況是我們只需要在記憶體中載入一個低解析度的圖片即可。
BitmapFactory提供了一些解碼的方法,用來從不同的資源中建立一個Bitmap。每一種解碼方法都可以通過BitmapFactory.Options設定一些附加的標記,以此來指定解碼選項。設定inJustDecodeBounds屬性為true可以在解碼的時候獲取到圖片的原始寬/高資訊,並不會真正的去載入圖片。
獲取到圖片的寬高意義何在呢?你可能會有這樣的疑問,事實上只要指定Options的inSampleSize並在decode時傳入Options,再將inJustDecodeBounds設定為false,就可以獲得壓縮後的Bitmap。而我們獲取了原始圖片的寬高,就可以根據我們所需要的寬高計算出壓縮的比例。例如一個ARGB_8888的Bitmap解析度為2048x1536直接載入所需的記憶體是12M,而指定inSampleSize為4,那麼會得到一個512x384的Bitmap,所需的記憶體僅為0.75M。
上程式碼:
public static Bitmap decodeBitmapFromResource(Resources res,int resId,int reqWidth,int reqHeight){
BitmapFactory.Options options = new BitmapFactory.Options();
//可以只獲取寬高而不載入
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(res,resId,options);
//計算壓縮比例
options.inSampleSize = calculateInSampleaSize(options,reqWidth,reqHeight);
//解碼
options.inJustDecodeBounds = false;
return BitmapFactory.decodeResource(res,resId,options);
}
private static int calculateInSampleaSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
// 圖片的高寬
final int height = options.outHeight;
final int width = options.outWidth;
//預設不壓縮
int inSampleSize = 1;
if (height > reqHeight || width > reqWidth) {
final int halfHeight = height / 2;
final int halfWidth = width / 2;
while ((halfHeight / inSampleSize) > reqHeight
&& (halfWidth / inSampleSize) > reqWidth) {
//這裡設定inSampleSize為2的冪是因為解碼器最終還是會對非2的冪的數進行向下處理,
// 獲取到最靠近2的冪的數
inSampleSize *= 2;
}
}
return inSampleSize;
}
複製程式碼
當我們使用ImageView載入期望大小為100*100畫素的圖片時就可以按照如下格式寫:
mImageView.setImageBitmap(
decodeBitmapFromResource(getResources(),R.id.drawa,100,100));
複製程式碼
壓縮的流程大概就到這了,繼續回到RecyclerView,上面的程式碼實現了和ListView類似的效果,接下來要實現的是瀑布流的效果。說的高大上,其實大致流程與之前差不多,不過改一下LayoutManager就行了,直接上完整的程式碼:
public class MainActivity extends AppCompatActivity {
private RecyclerView rv_test;
private ArrayList<ItemBean> itemList;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initData();
rv_test = (RecyclerView) findViewById(R.id.rv_test);
//設定layoutManager
rv_test.setLayoutManager(new StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL));
myAdapter mAdapter = new myAdapter();
mAdapter.setItemList(itemList);
rv_test.setAdapter(mAdapter);
}
private void initData() {
itemList = new ArrayList<ItemBean>();
itemList.add(new ItemBean(R.drawable.bird, "南小鳥1"));
itemList.add(new ItemBean(R.drawable.xiyangyang, "喜羊羊"));
itemList.add(new ItemBean(R.drawable.bird3, "南小鳥2"));
itemList.add(new ItemBean(R.drawable.blackcat, "黑貓"));
itemList.add(new ItemBean(R.drawable.dog, "狗與剪刀"));
itemList.add(new ItemBean(R.drawable.nike3, "妮可妮可"));
itemList.add(new ItemBean(R.drawable.paojie, "炮姐"));
itemList.add(new ItemBean(R.drawable.saber1, "吾王1"));
itemList.add(new ItemBean(R.drawable.saber2, "吾王2"));
}
class myAdapter extends RecyclerView.Adapter {
private ArrayList<ItemBean> ItemList;
ImageResizer imgResizer = new ImageResizer();
BitmapMemoryCache lruBitmapCache = new BitmapMemoryCache();
public void setItemList(ArrayList<ItemBean> ItemList) {
this.ItemList = ItemList;
}
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.layout_item, parent, false);
return new myViewHolder(view);
}
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
myViewHolder mholder = (myViewHolder) holder;
ItemBean it = itemList.get(position);
if(lruBitmapCache.getBitmapFromMemory(it.getDesc()) != null){
mholder.iv_item_img.setImageBitmap(lruBitmapCache.getBitmapFromMemory(it.getDesc()));
}else{
//快取圖片
lruBitmapCache.addBitmapToMemory(it.getDesc(),imgResizer.decodeSampledBitmapFromResource(getResources(),
it.getImg()));
mholder.iv_item_img.setImageBitmap(imgResizer.decodeSampledBitmapFromResource(getResources(),
it.getImg()));
}
mholder.tv_item_desc.setText(it.getDesc());
}
@Override
public int getItemCount() {
return ItemList.size();
}
class myViewHolder extends RecyclerView.ViewHolder {
ImageView iv_item_img;
TextView tv_item_desc;
public myViewHolder(View itemView) {
super(itemView);
iv_item_img = (ImageView) itemView.findViewById(R.id.iv_item_img);
tv_item_desc = (TextView) itemView.findViewById(R.id.tv_item_desc);
}
}
}
}
複製程式碼
效果圖:
其中有兩個類是之前沒有的,ImageResizer和BitmapMemoryCache,前者是用來壓縮的,後者是用來快取的。因為是瀑布流,所以我沒有設定期望的寬高,直接設定inSampleSize為4,壓縮之後圖片還是大了,在滑動的時候列表有卡頓現象,所以做了LruCache。大致就是這樣,程式碼如下:
ImageResizer
public class ImageResizer {
public Bitmap decodeSampledBitmapFromResource(Resources res, int resId) {
BitmapFactory.Options options = new BitmapFactory.Options();
//解析圖片而不會真正的載入
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(res, resId, options);
options.inSampleSize = 4;
options.inJustDecodeBounds = false;
return BitmapFactory.decodeResource(res,resId,options);
}
}
複製程式碼
BitmapMemoryCache
public class BitmapMemoryCache {
private final String TAG = "BitmapMemoryCache";
int maxMemory = (int)(Runtime.getRuntime().maxMemory() / 1024);
int catchSize = maxMemory / 8;
LruCache<String,Bitmap> lruCache = new LruCache<String,Bitmap>(catchSize){
@Override
protected int sizeOf(String key, Bitmap value) {
return value.getRowBytes() * value.getHeight() / 1024;
}
};
public void addBitmapToMemory(String key,Bitmap bitmap){
if(getBitmapFromMemory(key) == null){
lruCache.put(key,bitmap);
Log.i(TAG," "+maxMemory);
}
}
public Bitmap getBitmapFromMemory(String key){
return lruCache.get(key);
}
}
複製程式碼
做了快取之後,第一次滑動……說實話還是有點卡,但是之後的確沒有卡頓的現象了。果然人不能作死,沒事載入大圖作死不好!這裡因為我的圖比較少,從打的log來看我手機為一個app分配的記憶體是128M,1/8的記憶體16M來做快取夠了。如果圖片比較多,記憶體做快取不夠的話,可以考慮用DisLruCache,不過sdk裡是沒有的,需要去下。關於滑動的優化還沒有說完,不過今天暫時不打算繼續了,明天或者有空的時候繼續回來填這個坑。還有本來想寫個RecyclerView上拉重新整理的例子,可惜扯著扯著扯歪了,給忘了……有空再來來補上。
以後有機會再寫一篇關於快取的文吧,這裡就不多寫了。
最後本文原始碼可以在我的github獲取
在Android Studio中使用RecyclerView請在build.gradle中新增
compile 'com.android.support:recyclerview-v7:23.4.0'
複製程式碼
eclipse中請自行百度……
資料來源:《Android開發藝術探索》 Google官方培訓文件中文版