目的
少し前に、包括的な機能を備えたシンプルで使いやすい画像アップロードコンポーネントを作成しました。次に、アップロードした画像を画像コレクションの形式でアプリに表示しましょう。ユーザーエクスペリエンスを考慮して、新しい画像の読み込みには[無限]スクロールモードが採用されており、AndroidプラットフォームのRecyclerViewコンポーネントをお勧めします。
ImageView
ある。画像を表示するために自然なあなたがする必要がある。しかし、それはネットワークの絵の直接読み込みをサポートしていないローカルの画像を取得する(のような他のネットワークコンポーネントを通じてHttpURLConnection
、okhttp3
など)、取得しBitMap
たデータを、その後、setImageBitmap()
それらをロードします。
ImageViewにもsetImageURI(Uri uri)
メソッドがあります。ここでのURI名は架空のものですが、ローカルファイルパスにすることしかできません。
幸い、一部のオープンソースコンポーネントは、面倒なネットワーク操作とキャッシュ戦略をカプセル化し、使いやすいAPIを提供します。ここを選びましたGlide
。
成し遂げる
もっと読み込む
アイテムのレイアウト
2つあり、1つは加载更多/已全部加载
リスト内の各画像を表示するために使用され、もう1つはリストの最後に表示されてユーザーにプロンプトを表示します。
<!--图片-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<ImageView
android:id="@+id/thumbnail_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"/>
</LinearLayout>
<!--loadmore-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center">
<TextView
android:id="@+id/tv_load_more"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="正在加载更多" />
</LinearLayout>
RecyclerView.Adapter
RecyclerViewのデザインパターンに関する情報はたくさんあるので、ここでは繰り返しません。最初に実現しRecyclerView.Adapter
ます。
class ThumbnailListAdapter(
private val thumbnails: List<Thumbnail>,
private val totalCount: Long,
private val context: Context
) :
RecyclerView.Adapter<ThumbnailListAdapter.ThumbnailViewHolder>() {
// 调用若干次
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ThumbnailViewHolder {
// viewType就是通过getItemViewType得到的
val itemView = LayoutInflater.from(context).inflate(viewType, parent, false)
return ThumbnailViewHolder(itemView)
}
// 搞分页/瀑布加载的同学不要把这个和数据库的总数量搞混,这里的itemCount表示现在内存中数据量
// 我们可以[从后端]获取新数据添加到数据集,以实现loadmore功能
override fun getItemCount(): Int {
return if (thumbnails.isNotEmpty())
thumbnails.size + 1 // +1 是因为除了thumbnails数据集之外,还有个写死的loadmore项
else
0
}
// R.layout.xxx 是Int类型,可以直接返回
override fun getItemViewType(position: Int): Int {
return if (position < thumbnails.size)
R.layout.list_thumbnail_image // 正常图片显示
else
R.layout.list_loadmore_footer // 末尾loadmore
}
// 有屏幕外item进入屏幕时就会调用
override fun onBindViewHolder(holder: ThumbnailViewHolder, position: Int) {
if (position < thumbnails.size) {
Glide.with(context)
.load(thumbnails[position].uri)
.into(holder.itemView.thumbnail_view)
} else {
if (thumbnails.size >= totalCount)
holder.itemView.tv_load_more.text = "全部加载完毕"
}
}
// 必须这么继承一下
class ThumbnailViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)
}
スクロールモニター
RecyclerViewのスクロール監視を追加し、必要に応じて新しいデータをデータセットにロードします。
recyclerview.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
super.onScrollStateChanged(recyclerView, newState)
// 已经在加载则跳过
if (!_thumbnailsLoading) {
// 找到最后可见项的索引
val lastPos = layoutManager.findLastVisibleItemPosition()
val sum = adapter.itemCount
// 当快接近末尾项时(这里差额10,表示再显示10个item就没数据了)获取新数据
if (newState == RecyclerView.SCROLL_STATE_IDLE && sum - lastPos <= 10) {
vm.thumbnails.addAll(vm.getMoreAlbumCovers()) // 加载新数据到数据集中
_thumbnailsLoading = true
}
}
}
})
上記のプリロードされたデータをGlideのプリロードされた画像と混同しないでください。データを取得することと、データのURIを介して画像を取得してダウンロードすることは、2つのステップです。Glideは、RecyclerViewのプリロードソリューションを具体的に提供します。これは、スライド時にネットワークから要求されていない画像によって引き起こされるロードの待機を減らすことであり、現在、LinearLayoutManager
またはそのサブクラスレイアウトのみをサポートしています。
レイアウト
StaggeredGridLayoutManager
滝の列に写真を表示します。RecyclerViewのlayoutManagerをStaggeredGridLayoutManagerのインスタンスとして設定するだけです。StaggeredGridLayoutManagerは現在ベータ版であることに注意してください。
val sgLayoutManager =
StaggeredGridLayoutManager(3, StaggeredGridLayoutManager.VERTICAL)
recyclerview.layoutManager = sgLayoutManager
StaggeredGridLayoutManager
上下にスライドする過程で、画像ブロックが頻繁に再配置されることがわかります。オンラインの声明によると、これはViewHolder
再利用された画像とViewHolderによって読み込まれる画像のサイズに一貫性がないことが原因です。たとえば、ViewHolderの前に読み込まれた画像の高さが60で、その後リサイクルされましたが、サイズ情報は保持され、後で高さ80の画像によって再利用されました。StaggeredGridLayoutManagerがサイズに従ってレイアウトを並べ替えるためViewHolderの場合、サイズの変更により、より多くのサブソートが発生します。解決策はRecyclerView.Adapter.onBindViewHolder()
、次のように、ViewHolderがデータを(で)バインドするときに、このレイアウトの最終的なサイズを事前に設定することです。
override fun onBindViewHolder(holder: ThumbnailViewHolder, position: Int) {
val layoutParams =
holder.itemView.thumbnail_view.layoutParams as LinearLayout.LayoutParams
//手动设置ViewHolder高度
layoutParams.height = thumbnails[position].height
Glide.with(context).load(thumbnails[position].uri)
.into(holder.itemView.thumbnail_view)
}
上にスライドして戻ると、上(最初の行)の画像が再配置されることがよくあります。注意深く観察してください。これは、最初の行が最初に配置されたときに空席ではなく順番に配置され、後ろにスライドすると空席(空が最初にある場所)であるため、順序が最初の行と矛盾する可能性があるためです。並べ替え。幸いなことに、最終的には、画像のサイズに応じてそれぞれの位置に戻ります。そして、この状況は、それが最初に上にスライドして戻ったときにのみ発生します。
GreedoLayoutManager
StaggeredGridLayoutManagerには、合計3k行を超えるコードがあり、ベータ版でもあります。コードのクリーンさにこだわっています。GreedoLayoutManagerに目を向けました。これは、500px
画像の幅と高さの比率を維持しながら、複数の画像を1行につなぎ合わせて表示できる、オープンソースのLayoutManagerです。原理は非常に単純です。以下をご覧ください。アニメーション:
LayoutManagerの置き換えも非常に簡単で、RecyclerViewのlayoutManagerをリセットするだけです。
val layoutManager =
GreedoLayoutManager(adapter).also { it.setMaxRowHeight(resources.displayMetrics.heightPixels / 3) }
recyclerview.layoutManager = layoutManager
GreedoLayoutManagerは、レイアウトする前にアイテムのアスペクト比を知る必要がありSizeCalculatorDelegate
ます。アダプタにインターフェイスを実装させるだけです。
override fun aspectRatioForIndex(index: Int): Double {
val thumbnail = thumbnails[index]
return thumbnail.width / thumbnail.height.toDouble()
}
実行中のインターフェースは次のことを示しています。
それぞれの写真が予想よりもはるかに大きく、ほんの一部しか見えないことがわかります。この研究は、(ImageViewの組み込みのLinearLayout)上で定義されているポストレイアウトフォトギャラリーアイテム、最終プレゼンテーションを見つけ、のLinearLayoutのサイズは、各グリッドの大きさであり、組み込みImageViewののLinearLayoutを超えている場合、その最終的な寸法であるように思わMeasuredSize
- -我々は、onCreateViewHolder
使用時にそれを時間はLayoutInflater.from(context).inflate(viewType, parent, false)
、ここでparent
RecyclerViewであり、レイアウトXMLの幅と高さに設定されているmatch_parent
ImageViewののMeasuredSizeにも適合しなければならないRecyclerView-しかしImageViewのの最終的なサイズの幅と高さと同じになるように、グリッドサイズ。
例として幅を取り上げます。
期望:ImageView.width == LinearLayout.width == 网格.width
实际:ImageView.width == ImageView.measuredWith == RecyclerView.width
各フレームは、実際には傍受されたImageViewの左上隅であることがわかります。
いくつか検索した後、インターネット上の違いgetWidth
とgetMeasuredWidth
違いに関するさまざまな説明は私の混乱を解決しませんでした。この記事がソースコードを分析するまで、getWidth()とgetMeasuredWidth()の違いは私に知らせました、実際、Androidシステムはそうではありません幅を定義し、レイアウトをカスタマイズするときにサブアイテムのサイズを自由に設定でき、画面を超えるかどうかに制限はありません。このシナリオでは、GreedoLayoutManangerが最も外側のコントロール(ここではLinearLayout)の幅を処理した後、内側のコントロールの幅を再帰的に処理しなかったため、このバグが発生したと推定されます。
この場合、周辺機器のLinearLayoutを使用せず、ImageViewを直接使用すると、オーバーヘッドが少し節約されます。
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ThumbnailViewHolder {
return if (viewType == 0) {
val imageView = ImageView(parent.context).apply {
scaleType = ImageView.ScaleType.CENTER_CROP
layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
}
ThumbnailViewHolder(imageView)
} else {
val itemView = LayoutInflater.from(context).inflate(viewType, parent, false)
ThumbnailViewHolder(itemView)
}
}
もちろん、ViewHolderの再利用による表示の問題もあります。画像はその一部のみを示しており、再利用前のViewHolderのアスペクト比に従って次のように表示されます。
公式のGlideドキュメントで提案されているwaitForLayout()を使用して、それを掘り下げるのが面倒override(width, height)
です。それを解決するために、事前に画像サイズを伝えてください。
Glide.with(context)
.load(thumbnails[position].uri)
.override(thumbnails[position].width, thumbnails[position].height)
.into(holder.itemView as ImageView)
// .waitForLayout() //并没有用
プルダウンして更新
クリックするかどうかにかかわらず、SwipeRefreshLayoutを簡単に使用できます。最終製品は次のとおりです
その他
一般的に使用されているdetachAndScrapView
RecyclerViewは、将来View [Holder]を再利用するロジックを自動的に処理するのに役立ちます。ただし、一部のシナリオ(現在表示されているビューを削除するのではなく再配置するなど)では、より軽量なものを使用できますdetachView
(デタッチ後、ビューはインターフェイスに表示されません)が、次のレイアウトの前に手動で呼び出すことを忘れないでくださいattachView
(場所に関しては、取り外し前と取り付け後の場所)またはremoveDetachedView
/ recycleView
。
デタッチ後、RecyclerView.getChildCount()はそれに応じて減少することに注意してください。
ビューレイアウトをインターフェイスに実際に配置するのは、RecyclerViewlayoutDecorated
メソッドです。