在 iOS 中,我們通過 UICollectionView 可以靈活的進行排版,這次打算通過 GridLayout 搭配按鈕來進行排版的切換。
Components
- RecyclerView
- GridLayoutManager
Layout Switch
這次除了將內容切成 MVC 另外還自訂了一個 Menu (Toolbar)
在 Toolbar 上面放置一個切換用的按鈕(右邊)讓 Activity 去處理切換 Layout 的功能。
Menu
建立檔案 /res/menu/menu_main.xml 為 Menu 提供一個按鈕用來切換 Layout
1 2 3 4 5 6 7 8 |
<menu xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"> <item android:id="@+id/menu_switch_layout" android:title="switch" app:showAsAction="always" android:icon="@drawable/icon_menu_1"/> </menu> |
在 MainActivity 中實現 Switch 功能
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
// 替換成我們的 menu layout override fun onCreateOptionsMenu(menu: Menu?): Boolean { menuInflater.inflate(R.menu.menu_main, menu) return super.onCreateOptionsMenu(menu) } // 當點擊 Switch 的時候做對應的事件處理 override fun onOptionsItemSelected(item: MenuItem?): Boolean { if(item!!.itemId == R.id.menu_switch_layout){ switchLayout() switchIcon(item) return true } return super.onOptionsItemSelected(item) } |
Switch 功能實現
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
// 切換 Layout 並重新 render 畫面 private fun switchLayout() { if (gridLayoutManager.spanCount == 1) { gridLayoutManager.spanCount = 2 } else { gridLayoutManager.spanCount = 1 } itemsAdapter.notifyItemRangeChanged(0, itemsAdapter.getItemCount()) } // 切換 Switch 圖標 private fun switchIcon(item: MenuItem) { if (gridLayoutManager.spanCount == 2) { item.icon = resources.getDrawable(R.drawable.icon_menu_1) } else { item.icon = resources.getDrawable(R.drawable.icon_menu_2) } } |
Layout
準備好兩種佈局方案來進行切換,這兩個 Layout 都設定寬度為 match_parent 之後再使用的時候再給不同的寬達成我們要的效果。
res/layout/layout_item_big(左邊) / res/layout/layout_item_small(右邊)
Model
1 |
data class ItemModel(var name:String, var likeCount:Int, var commentCount:Int, var image:Int) |
Adapter
我們自己定義了兩種 View Type
1 2 |
val VIEW_TYPE_SMALL = 1 val VIEW_TYPE_BIG = 2 |
通過 override getItemViewType() 來重新定義 viewType 對應的 Int
1 2 3 4 5 6 7 8 |
override fun getItemViewType(position: Int): Int { val spanCount = layoutManager.spanCount when(spanCount){ 2 -> return VIEW_TYPE_SMALL else -> return VIEW_TYPE_BIG } } |
onCreateViewHolder 中,根據 View Type 指定 Layout 文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
// 入口 override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { val metrics = parent.context.resources.displayMetrics // 指定了 layout when(viewType){ VIEW_TYPE_SMALL -> { println("create view holder small") val view = LayoutInflater.from(parent.context).inflate(R.layout.layout_item_small, parent, false) view.minimumWidth = 900 * (1080 / metrics.widthPixels) view.minimumHeight = 220 return ViewHolder(view, viewType) } else -> { println("create view holder big") val view = LayoutInflater.from(parent.context).inflate(R.layout.layout_item_big, parent, false) view.minimumWidth = metrics.widthPixels - 16 view.minimumHeight = 220 return ViewHolder(view, viewType) } } } |
class ViewHolder 中,根據 View Type 綁定對應的 Layout 元件,並賦予值。
其實在這裡,一開始因為引入的圖片比較大,一下子就出現了 Out of memory 的警告,後來查可以通過 BitmapFacotry 來解決。
但因為一天研究一個內容的時間有限,我這裡先直接將 1024 x 768 的圖片替換成 512 x 384 的小圖,文章後面會提到 memory 計算的內容。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
// view inner class ViewHolder(itemView: View, var viewType:Int) : RecyclerView.ViewHolder(itemView){ var imageView: ImageView? = null var nameTextView: TextView? = null var likeTextView: TextView? = null var commentTextView: TextView? = null fun bindModel(item:ItemModel){ // set description when(viewType){ VIEW_TYPE_SMALL -> { imageView = itemView.findViewById(R.id.smallImageView) nameTextView = itemView.findViewById(R.id.smallNameTextView) } else -> { imageView = itemView.findViewById(R.id.bigNameImageView) nameTextView = itemView.findViewById(R.id.bigNameTextView) likeTextView = itemView.findViewById(R.id.likeTextView) commentTextView = itemView.findViewById(R.id.commentTextView) } } imageView?.setImageResource(item.image) nameTextView?.setText(item.name) likeTextView?.setText("Likes: ${item.likeCount}") commentTextView?.setText("comments: ${item.commentCount}") } } |
OOM(Out Of Memory)
在寫這個應用的時候遇到了一件事情,我準備了 20 張大小差不多是 100kb 左右的圖片,長寬大概是 1024 x 800
結果打開 App 以後非常快的就碰到了記憶體不足的問題
1 |
java.lang.OutOfMemoryError: Failed to allocate a 21233676 byte allocation with 5688920 free bytes and 5MB until OOM |
而當我換成一系列 50kb 左右的圖片,長寬約為 512 x 400 的圖片時,
明明兩種圖片都很小,只是解析度變了就沒有出現過 OOM了。
後來專門查了一下才發現,原來加載圖片所佔用的 Memory 和檔案的大小是不一致的。
Bitmap
圖片在電腦上是以位圖(bitmap)的形式存在的,而位圖是一個矩形點陣,每一個點我們稱為像素也就是 pixel.
一張 MxN 大小的圖,是由 MxN 個明、暗度像素所組成的。
而每一個像素根據明暗度的不同用灰度值 (Gray Level) 來表示,將白色的灰度值定為 255、黑色定為 0.
而彩色圖片是由 R G B 三個單色圖像組成。
色彩的存儲方式
A 代表 Alpha(透明度) RGB 分別是 Red Green Blue
- ARGB_4444 – ARGB 分別佔用 4位,合起來就是 16位,也就是 2 字節。
- ARGB_8888 – ARGB 分別佔用 8位,合起來就是 32位,也就是 4 字節。
- ALPHA_8 – 只有 A 佔用了4位,僅表示透明度而沒有色彩,佔用 1 字節。
- RGB_565 – RGB 分別佔用 5 6 5 位,不表示透明度,共佔用 16 位,佔用 4 字節。
佔用的位數越多意味著可以存儲的色彩越豐富,但佔用的記憶體同樣也更多。
計算一張圖片佔用的 Memory
假設我們用的其中一張圖片為 1024 x 768 pixel 格式為 ARGB_8888
那麼每一個像素佔用的是 8 + 8 + 8 + 8 = 32 位 = 4 字節
而一張圖片佔用的 memory 就是 1024 * 768 * 4 / 1024 = 3072 KB = 3MB.
所以當我加載 20 張圖片的時候,直接就佔用了 60MB 的記憶體。
設備給 App 分配的 Memory
通過 ActivityManager 我們可以知道設備給 App 分配了多少 Memory 來使用,單位是 MB.
1 2 |
val activityManager = getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager val memory = activityManager.memoryClass |
但實際上在 allocate memory 給圖片的時候,似乎看的不是上面所給的 memory
還有另外一種 Memory 查詢功能,不過看從數字來看是設備的 Memory
1 2 3 4 |
val memoryInfo = ActivityManager.MemoryInfo() println("total memory is ${memoryInfo.totalMem / 1024 / 1024} MB") println("available memory is ${memoryInfo.availMem / 1024 / 1024} MB") println("threshold memory is ${memoryInfo.threshold / 1024 / 1024} MB") |
筆記
- 研究: 了解緩存方案 LruCache
- 研究: 了解 Memory 機制
- TODO: 嘗試通過 Glide 來處理圖片
參考
- 官方文件 – Handling Bitmaps
- Android 高效加載圖片,避免 OOM
- 可以到 Gtihub 上看對應的 Source Code