私はスクロールバーで高さの異なるアイテムを含むRecyclerView
を持っています。アイテムの高さが異なるため、現在表示されているアイテムに応じて、スクロールバーは垂直サイズを変更します(スクリーンショットを参照)。問題を表示するサンプルプロジェクト here を作成しました。
EDIT:スクロールバーの位置と高さは、RecyclerViews
computeVerticalScrollOffset
、computeVerticalScrollRange
およびcomputeVerticalScrollExtent
。しかし、スクロールバーを動的なアイテムの高さで適切に機能させるためにこれらを実装する方法についてはわかりません。
問題は、私が思うに、RecyclerView
は現在表示されているアイテムに基づいてすべてのアイテムの合計高さを推定し、それに応じてスクロールバーの位置と高さを設定することです。これを解決する1つの方法は、すべてのアイテムの合計の高さをより正確に見積もることです。
この状況を処理する最良の方法は、各アイテムのサイズに基づいてスクロールバーの範囲を何らかの方法で計算することです。それは実用的でも望ましいことでもありません。その代わりに、カスタムRecyclerViewの簡単な実装を以下に示します。これを使用して、必要なものを取得することができます。さまざまなスクロール方法を使用してスクロールバーを制御する方法を示します。表示されるアイテムの数に基づいて、親指のサイズを初期サイズに固定します。覚えておくべき重要なことは、スクロール範囲は任意ですが、他のすべての測定(範囲、オフセット)は同じ単位を使用する必要があることです。
computeVerticalScrollRange()
のドキュメントを参照してください。
これは結果のビデオです。
更新:いくつかの問題を修正するためにコードが更新されました:親指の動きがぎくしゃくしなくなり、親指が下に止まるようになりましたRecyclerView
が一番下までスクロールするため。また、コードの後に記載されているいくつかの警告があります。
MyRecyclerView.Java(更新済み)
_public class MyRecyclerView extends RecyclerView {
// The size of the scroll bar thumb in our units.
private int mThumbHeight = UNDEFINED;
// Where the RecyclerView cuts off the views when the RecyclerView is scrolled to top.
// For example, if 1/4 of the view at position 9 is displayed at the bottom of the RecyclerView,
// mTopCutOff will equal 9.25. This value is used to compute the scroll offset.
private float mTopCutoff = UNDEFINED;
public MyRecyclerView(Context context) {
super(context);
}
public MyRecyclerView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public MyRecyclerView(Context context, @Nullable AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
/**
* Retrieves the size of the scroll bar thumb in our arbitrary units.
*
* @return Scroll bar thumb height
*/
@Override
public int computeVerticalScrollExtent() {
return (mThumbHeight == UNDEFINED) ? 0 : mThumbHeight;
}
/**
* Compute the offset of the scroll bar thumb in our scroll bar range.
*
* @return Offset in scroll bar range.
*/
@Override
public int computeVerticalScrollOffset() {
return (mTopCutoff == UNDEFINED) ? 0 : (int) ((getCutoff() - mTopCutoff) * ITEM_HEIGHT);
}
/**
* Computes the scroll bar range. It will simply be the number of items in the adapter
* multiplied by the given item height. The scroll extent size is also computed since it
* will not vary. Note: The RecyclerView must be positioned at the top or this method
* will throw an IllegalStateException.
*
* @return The scroll bar range
*/
@Override
public int computeVerticalScrollRange() {
if (mThumbHeight == UNDEFINED) {
LinearLayoutManager lm = (LinearLayoutManager) getLayoutManager();
int firstCompletePositionw = lm.findFirstCompletelyVisibleItemPosition();
if (firstCompletePositionw != RecyclerView.NO_POSITION) {
if (firstCompletePositionw != 0) {
throw (new IllegalStateException(ERROR_NOT_AT_TOP_OF_RANGE));
} else {
mTopCutoff = getCutoff();
mThumbHeight = (int) (mTopCutoff * ITEM_HEIGHT);
}
}
}
return getAdapter().getItemCount() * ITEM_HEIGHT;
}
/**
* Determine where the RecyclerVIew display cuts off the list of views. The range is
* zero through (getAdapter().getItemCount() - 1) inclusive.
*
* @return The position in the RecyclerView where the displayed views are cut off. If the
* bottom view is partially displayed, this will be a fractional number.
*/
private float getCutoff() {
LinearLayoutManager lm = (LinearLayoutManager) getLayoutManager();
int lastVisibleItemPosition = lm.findLastVisibleItemPosition();
if (lastVisibleItemPosition == RecyclerView.NO_POSITION) {
return 0f;
}
View view = lm.findViewByPosition(lastVisibleItemPosition);
float fractionOfView;
if (view.getBottom() < getHeight()) { // last visible position is fully visible
fractionOfView = 0f;
} else { // last view is cut off and partially displayed
fractionOfView = (float) (getHeight() - view.getTop()) / (float) view.getHeight();
}
return lastVisibleItemPosition + fractionOfView;
}
private static final int ITEM_HEIGHT = 1000; // Arbitrary, make largish for smoother scrolling
private static final int UNDEFINED = -1;
private static final String ERROR_NOT_AT_TOP_OF_RANGE
= "RecyclerView must be positioned at the top of its range.";
}
_
警告実装によっては、次の問題に対処する必要がある場合があります。
サンプルコードは、垂直スクロールでのみ機能します。また、サンプルコードは、RecyclerView
の内容が静的であることを前提としています。 RecyclerView
をサポートするデータを更新すると、スクロールの問題が発生する可能性があります。 RecyclerView
の最初の全画面に表示されるビューの高さに影響する変更が行われた場合、スクロールはオフになります。以下の変更はおそらく正常に動作します。これは、コードがスクロールオフセットを計算する方法が原因です。
スクロールオフセットのベース値(変数mTopCutOff
)を決定するには、computeVerticalScrollRange()
が最初に呼び出されたときにRecyclerViewを一番上までスクロールして、ビューを測定できるようにする必要があります。それ以外の場合、コードは「IllegalStateException」で停止します。これは、RecyclerView
がまったくスクロールされている場合、向きの変更で特に問題になります。これを回避する簡単な方法は、スクロール位置の復元を禁止することです。これにより、方向が変更されたときにデフォルトで上部に戻ります。
(以下はおそらく最良の解決策ではありません...)
_var lm: LinearLayoutManager = object : LinearLayoutManager(this) {
override fun onRestoreInstanceState(state: Parcelable?) {
// Don't restore
}
}
_
これがお役に立てば幸いです。 (ところで、MCVEはこれをはるかに簡単にしました。)
スクロールの進行状況のメトリックとしてアイテムの位置を使用します。これにより、スクロールインジケーターが少し不安定になりますが、少なくとも固定サイズのままです。
RecyclerViewのカスタムスクロールインジケーターの実装は複数あります。ほとんどは高速スクローラーの2倍です。
RecyclerViewFastScrollerライブラリ に基づいた 自分の実装 を次に示します。基本的に、ScrollViewやDrawerLayoutと同様に、アニメーション化されるカスタムビューサブクラスを作成する必要があります。
View#offset*
呼び出しによるサムビューのオフセット位置たぶん、今すぐその魔法のすべてを学びたくはないでしょう。既存の高速スクロールライブラリ(RecyclerViewFastScrollerまたはそのクローンの1つ)を使用するだけです。
属性を間違えない場合はAndroid:scollBarSize="Xdp"
が動作するはずです。 RecyclerView
xmlに追加します。
その方法でサイズを決定すると、固定されたままになります。