RecyclerView.LayoutManagerに぀いおのAndroidたたはもう䞀床の銬力







著者によるず、この蚘事は、このような゚キサむティングな分野で最初の䞀歩を螏み出しおいるAndroid開発者を始めたばかりの人に圹立぀かもしれたせん。 この蚘事の䞻題の歎史は、トレヌニングプロゞェクトにいわゆる「すごい効果」を䞎えるずいうアむデアから始たりたした。 これがどれほど成功したかを刀断できたす。 私は猫の䞋ですべおの奜奇心を尋ねたす。



このすべおの䞍名誉を䌎うデモプロゞェクトは、GitHubのリンクにありたす。



私たちが興味を持っおいる画面は、最愛のRecyclerViewに基づいおいたす。 しかし、ハむラむトは、リストをスクロヌルするず、1぀の完党に衚瀺される䞊郚芁玠が特別な方法で拡倧瞮小されるこずです。 この機胜の特城は、リストアむテムを構成するコンポヌネントのスケヌリングが異なるこずです。



ただし、䞀床芋たほうがいいです。



図 1.䞀般的なビュヌ








リスト項目を詳现に怜蚎しおください。 プロゞェクトでは、CardViewから継承されたクラスLaunchItemViewずしお実装されたす。 そのマヌクアップには、次のコンポヌネントが含たれおいたす。











図 2.リスト項目の構造LaunchItemView。



リストをスクロヌルする過皋で、次のこずが発生したす。



  1. 芁玠の高さは、最小倀装食付きのタむトルの高さに等しいから最倧倀装食付きのタむトルず説明テキストを衚瀺できる高さに等しいに倉化したす。
  2. 画像の高さは芁玠の高さから装食を匕いたものに等しく、幅はそれに比䟋しお倉化したす。
  3. 芁玠内の盞察䜍眮ず説明テキストのサむズは倉曎されたせん。
  4. スケヌリング倀は、装食の察象ずなるすべおのコンテンツを衚瀺するのに十分な最小サむズによっお䞊から制限され、装食の察象ずなるタむトルを衚瀺するのに十分な最小サむズによっお䞋から制限されたす。
  5. 境界倀以倖のスケヌリングは、䞊郚の完党に衚瀺されるリストアむテムに適甚されたす。 その䞊にある芁玠には最倧スケヌル、䞋にある芁玠-最小芁玠がありたす。


したがっお、䞊方向にスクロヌルするず、芁玠のコンテンツが埐々に拡倧され、むメヌゞが比䟋的に拡倧されたす。 䞋にスクロヌルするず、逆の効果が芋られたす。



RecyclerViewのLayoutManagerず2぀の远加コンポヌネントを䜜成しお、このようにタスクを蚭定したす。 しかし、たず最初に。



LaunchLayoutManager



私のトレヌニングプロゞェクトはスペヌスの問題に専念しおいるため、コンポヌネントには察応する名前が付けられおいたす。



任意のLayoutManagerを䜜成するトピックを研究しお、このトピックに関する2぀の良い蚘事を芋぀けたした[1、2]。 内容を繰り返したせん。 代わりに、決定の最も興味深い点に぀いお説明したす。



問題の分解を実行しお、2぀の䞻芁な段階を特定したした。



  1. 画面䞊に完党たたは郚分的に衚瀺される最初ず最埌の芁玠のむンデックスを決定したす。
  2. 必芁なスケヌリングで可芖芁玠をレンダリングしたす。


䞀般に、リストアむテムの堎所ずサむズは次のずおりです。









図3. RecyclerViewずその芁玠。



可芖芁玠のむンデックスの閉じた間隔の定矩



図 3からvisibleは、3から11たでのむンデックスを持぀芁玠です。 さらに、このアルゎリズムによれば、むンデックス0〜3の芁玠は最倧サむズ、芁玠5〜12は最小サむズ、むンデックス4の芁玠は最小ず最倧の䞭間です。



ご想像のずおり、可芖芁玠の最小および最倧むンデックスを決定する際の重芁なポむントの1぀は、可芖領域の䞊郚境界に察しおリストがスクロヌルされるオフセットです。



これらの倀を決定するために蚭蚈されたcalculateVisiblePositionsメ゜ッドを怜蚎しおください。



1 private void calculateVisiblePositions() { 2 if (mBigViewHeight != 0) { 3 mMaximumOffset = (getItemCount() - 1) * mBigViewHeight; 4 mFirstVisibleViewPosition = mOffset / mBigViewHeight; 5 if (mFirstVisibleViewPosition > getItemCount() - 1) { 6 mFirstVisibleViewPosition = 0; 7 mOffset = 0; 8 } 9 10 mLastVisibleViewPosition = mFirstVisibleViewPosition; 11 int emptyHeight = getHeight(); 12 mFirstVisibleViewTopValue = mBigViewHeight * mFirstVisibleViewPosition - mOffset; 13 int firstVisibleViewBottomValue = mFirstVisibleViewTopValue + mBigViewHeight; 14 emptyHeight -= firstVisibleViewBottomValue; 15 int secondVisibleViewHeight = getViewHeightByTopValue(firstVisibleViewBottomValue); 16 if (emptyHeight - secondVisibleViewHeight >= 0) { 17 emptyHeight -= secondVisibleViewHeight; 18 mLastVisibleViewPosition++; 19 int smallViewPosCount = emptyHeight / mSmallViewHeight; 20 mLastVisibleViewPosition += smallViewPosCount; 21 emptyHeight -= smallViewPosCount * mSmallViewHeight; 22 if (emptyHeight > 0) { 23 mLastVisibleViewPosition++; 24 } 25 } 26 if (mLastVisibleViewPosition > getItemCount() - 1) { 27 mLastVisibleViewPosition = getItemCount() - 1; 28 } 29 Timber.d("calculateVisiblePositions mFirstVisibleViewPosition:%d, mLastVisibleViewPosition:%d", mFirstVisibleViewPosition, mLastVisibleViewPosition); 30 } 31 }
      
      





2行目-すべおのコンテンツ芋出し、テキストず画像の説明を衚瀺する最倧サむズの芁玠の高さが決定されおいるかどうかを確認したす。 そうでない堎合は、続行しおも意味がありたせん。



行3-リストのすべおの芁玠をスクロヌルアップするスペヌスの量を蚈算したす。ただし、1぀を陀き、最倧蚱容オフセットです。 この倀は、scrollVerticallyByメ゜ッドのオフセット倀を制限したす。



行4-最初の可芖芁玠のむンデックスを蚈算したす。 倉数mFirstVisibleViewPositionは敎数型に属するため、小数郚分を砎棄するこずにより、郚分的に衚瀺される最初の芁玠のケヌスを自動的に考慮したす。



5〜8行目-最初に衚瀺されおいる芁玠のむンデックスが、リスト内で最埌に䜿甚可胜な芁玠のむンデックスを超えおいるかどうかがチェックされたす。 これは、たずえば、リストが最初にスクロヌルアップされ、次にフィルタヌを適甚するなどしお芁玠の数が枛少した堎合に発生する可胜性がありたす。 この堎合、リストを最初に「巻き戻す」だけです。



10行目-最初の衚瀺芁玠のむンデックスを、最埌のむンデックスを芋぀けるための開始点ずしお䜿甚したす。



行11-衚瀺領域の高さを蚭定したす。 この倀は、可芖芁玠の最倧むンデックスの怜玢䞭に枛少したす。



行12、13-最初の芁玠の描画長方圢の䞊䞋の座暙を決定したす。



14行目-最初の芁玠の可芖郚分のサむズだけ、空き可芖領域の量を枛らしたす。 ぀たり 仮想的に最初の芁玠を画面に配眮するかのように。



行15-2番目の衚瀺芁玠の高さを蚈算したす。 この芁玠は、スケヌリングの察象ずなる可胜性がありたすアルゎリズムの条項5を参照。 getViewHeightByTopValueメ゜ッドの詳现を以䞋に説明したす。



16行目-画面䞊の2番目の芁玠の「仮想配眮」の埌にただ空きスペヌスがあるかどうかを確認したす。



17行目-残りの空き容量を修正したす。



18行目-最埌に衚瀺されおいる芁玠のむンデックスをむンクリメントしたす。



19行目-残りの空き領域に収たり、同時に完党に芋える最小サむズの芁玠の最倧数を蚈算したす。



20行目-最埌に衚瀺された芁玠のむンデックスを蚈算倀で増やしたす。



21〜24行目-別の芁玠を郚分的に配眮する堎所があるかどうかを確認したす。 その堎合、むンデックスをもう1぀増やしたす。



次に、画面䞊の䜍眮に応じお2番目の衚瀺芁玠の高さを蚈算する方法に぀いお説明したす。この芁玠の衚瀺長方圢の䞊郚座暙です。



 1 private int getViewHeightByTopValue(int topValue) { 2 topValue -= mTopAndBottomMargins; 3 if (topValue > mBigViewHeight) { 4 topValue = mBigViewHeight; 5 } else if (topValue < 0) { 6 topValue = 0; 7 } 8 float scale = 1 - (float) topValue / mBigViewHeight; 9 int height = (int) (mSmallViewHeight + scale * (mBigViewHeight - mSmallViewHeight)); 10 Timber.d("getViewHeightByTopValue topValue:%d, scale:%f, height:%d", topValue, scale, height); 11 return height; 12 }
      
      





2行目-䞋マヌゞンず䞊マヌゞンをドロップしたす。



行3〜7-スケヌルを正しく蚈算するために、topの倀の䞊から芁玠の最倧の高さたで、および䞋かられロたでを制限したす。



8行目-スケヌリング係数を蚈算したす。これは、最倧に展開された芁玠に察しお1の倀をずり、最小倀に察しお0を取りたす。 この特定の結果を正確にするには、3〜7行目の制限が必芁です。



9行目-スケヌリング係数を考慮しお、最小高さの远加ずしお芁玠の高さを蚈算し、最倧高さず最小高さの差を蚈算したす。 ぀たり 係数0-最小高さ、および1-最小+最倧-最小=最倧。



これで、描画する芁玠の最初ず最埌のむンデックスがわかりたした。 それをする時間です



必芁なスケヌリングで芁玠を描画する



レンダリングプロセスは本質的に呚期的であるため、レンダリングの盎前に、既存のRecyclerView芁玠でキャッシュをりォヌムアップしたすもしあれば。 このような手法は[1、2]で説明されおおり、ここでは詳しく説明したせん。



fillDownメ゜ッドを怜蚎したす。このメ゜ッドは、䜿甚可胜な可芖領域に沿っお䞊から䞋に移動する芁玠を描画するように蚭蚈されおいたす。



 1 private void fillDown(RecyclerView.Recycler recycler) { 2 boolean isViewFromCache; 3 int topValue = mFirstVisibleViewTopValue; 4 int bottomValue; 5 int viewHeight; 6 try { 7 for (int curPos = mFirstVisibleViewPosition; curPos <= mLastVisibleViewPosition; curPos++) { 8 isViewFromCache = true; 9 View view = mViewCache.get(curPos); 10 if (view == null) { 11 isViewFromCache = false; 12 view = recycler.getViewForPosition(curPos); 13 } else { 14 mViewCache.remove(curPos); 15 } 16 viewHeight = getViewHeightByTopValue(topValue); 17 bottomValue = topValue + viewHeight; 18 if (view instanceof LaunchItemView) { 19 ((LaunchItemView) view).updateContentSize(viewHeight); 20 } 21 if (isViewFromCache) { 22 if (view.getTop() != topValue) { 23 view.setTop(topValue); 24 } 25 if (view.getBottom() != bottomValue - mTopAndBottomMargins) { 26 view.setBottom(bottomValue - mTopAndBottomMargins); 27 } 28 attachView(view); 29 } else { 30 layoutView(view, topValue, bottomValue); 31 } 32 topValue = bottomValue; 33 } 34 } catch (Throwable throwable) { 35 Timber.d(throwable); 36 } 37 }
      
      





3行目-最初の可芖芁玠の䞊郚座暙で倉数topValueを開始したす。 この䟡倀のストヌブから、私たちは螊り続けたす。



7行目-描画する芁玠のむンデックスによっおサむクルを開始したす。



8行目-キャッシュに必芁なアむテムが芋぀かるず楜芳的です。



9行目-キャッシュを調べたす垌望がある堎合。



10-12行目-キャッシュに必芁な芁玠が芋぀からなかった堎合、RecyclerView.Recyclerクラスのむンスタンスからそれを芁求したす。これは、特定の䜍眮のアダプタヌからのデヌタで初期化されたビュヌを返したす。



14行目-芁玠がただキャッシュ内にあった堎合、そこから削陀したす。



行16-画面䞊の䜍眮に応じお芁玠の高さを蚈算したす。



17行目-芁玠の䞋の境界線を蚈算したす。



18〜20行目-実行方法がわかっおいる堎合、芁玠のコンテンツをスケヌリングしたす。



21行目-珟圚のビュヌキャッシュから取埗が以前にレンダリングされたかどうか、たたは新しいむンスタンスを取埗したかどうかを理解するこずが重芁です。 これら2぀のオプションには、異なるアプロヌチが必芁です。



行22〜28-ビュヌがキャッシュから取埗される堎合、必芁に応じお、䞊郚ず䞋郚の座暙の倀を倉曎し、ビュヌをアタッチしたす。



30行目-ビュヌがキャッシュからではない堎合、芁玠を衚瀺するには、layoutViewメ゜ッドを䜿甚したす。これに぀いおは以䞋で説明したす。



行32-topValueを新しく描画されたビュヌの䞋偎の境界にシフトしお、この倀がルヌプの次の反埩の開始点になるようにしたす。



次に、RecyclerView.Recyclerクラスのむンスタンスから取埗した新しいリストアむテムを衚瀺するように蚭蚈されたlayoutViewメ゜ッドに぀いお説明したす。



 1 private void layoutView(View view, int top, int bottom) { 2 addView(view); 3 measureChildWithMargins(view, 0, 0); 4 int decoratedMeasuredWidth = getDecoratedMeasuredWidth(view); 5 RecyclerView.LayoutParams layoutParams = (RecyclerView.LayoutParams) view.getLayoutParams(); 6 7 layoutDecorated(view, 8 layoutParams.leftMargin, 9 top + layoutParams.topMargin, 10 decoratedMeasuredWidth + layoutParams.rightMargin, 11 bottom + layoutParams.bottomMargin); 12 }
      
      





2行目-RecyclerViewにビュヌを远加したす。



3行目-ビュヌを枬定したす。



行4-ビュヌの幅を定矩したす。



5行目-ビュヌレむアりトオプションを取埗したす。



行7-実際に受信した座暙でビュヌを描画したす。



コンテンツのスケヌリング



リスト項目の構造党䜓のうち、画像のみがスケヌリングの察象です。 このスケヌリングのロゞックは、Viewから継承されたScaledImageViewクラスにカプセル化されおいたす。



この堎合、スケヌリングは任意の時点で必芁であり、ナヌザヌがリストをどれだけ集䞭的にスクロヌルするかなど、制埡できない倖郚芁因に䟝存したす。 これはリアクティブプログラミングパラダむムに非垞に自然に適合するため、RxJavaずホットデヌタ゜ヌスで緎習する機䌚を逃すこずはできたせんでした。



PublishProcessorを䜿甚しお、目的の画像の高さを決定する敎数倀のストリヌムを䜜成したす。



 private PublishProcessor<Integer> mScalePublishProcessor;
      
      





したがっお、スケヌリングを実行するには、必芁な倀を持぀別のストリヌム芁玠を生成するだけです。



 public void setImageHeight(int height) { mScalePublishProcessor.onNext(height); }
      
      





そしお、このストリヌムの非同期凊理がどのように行われるかを以䞋に瀺したす。



 1 private void initObserver() { 2 mScalePublishProcessor 3 .filter(value -> value > 0 && value != mBitmapHeight && mOriginalBitmap != null) 4 .onBackpressureBuffer(1, 5 () -> Timber.d("initObserver: buffer overflow"), BackpressureOverflowStrategy.DROP_OLDEST) 6 .observeOn(Schedulers.computation(), false, 1) 7 .map(this::createScaledBitmap) 8 .map(this::setScaledBitmap) 9 .subscribe( 10 (value) -> { 11 invalidate(); 12 Timber.d("initObserver invalidate ThreadId:%d", Thread.currentThread().getId()); 13 }, Timber::d); 14 }
      
      





行3-初期フィルタリングを実行したす。





4行目-バッファヌサむズ1゚レメントのバックプレッシャヌず、バッファヌから叀い゚レメントを絞り出す戊略を䜿甚したす。 このため、スケヌリングに最も関連する高さの倀を垞に取埗したす。 私たちの堎合、これは非垞に重芁です。なぜなら、ナヌザヌのアクションたずえば、リストの集䞭的なスクロヌルに応答しお、芁玠を凊理するよりも速く芁玠を生成するスケヌリングを実行するホット゜ヌスがあるためです。 このような状況では、倀をバッファに蓄積しおこれらの芁玠を順番に凊理するこずは意味がありたせん。これらの芁玠は既に叀くなっおいるため、ナヌザヌはすでにこの状態を「抌し぀ぶし」おいたす。



効果を説明および匷化するために、画像のスケヌリング方法createScaledBitmapに25ミリ秒の遅延を远加し、以䞋の2぀の芖芚化を行いたした。背圧なし巊ず背圧あり右。 巊偎のむンタヌフェヌスは明らかにナヌザヌのアクションの背埌にあり、䜕らかの独自の生掻を送っおいたす。 正しい-スケヌリング方法の远加の遅延により滑らかさが倱われたしたが、応答性はありたせん。



比范
背圧なし 背圧




6行目-バッファヌのサむズで䜜業をSchedulers.computationストリヌムに転送したす。



7行目-スケヌリングを実行したす以䞋の方法の説明を参照。



8行目-衚瀺するスケヌリング画像を蚭定したす。



9行目-ストリヌムをサブスクラむブしたす。



11行目-スケヌリングの最埌に、芁玠を再描画したす。



createScaledBitmapメ゜ッド。目的のサむズの画像の取埗に盎接関䞎したす。



 1 private Bitmap createScaledBitmap (Integer value) { 2 Timber.d("createScaledBitmap value:%d", value); 3 if (value > mHeightSpecSize) { 4 value = mHeightSpecSize; 5 } 6 return Bitmap.createScaledBitmap(mOriginalBitmap, value, value, false); 7 }
      
      





行3〜5-最倧高さをビュヌサむズに制限したす。これは、onMeasureメ゜ッドで蚈算されたす。



6行目-元の画像から目的のサむズの画像を䜜成したす。



setScaledBitmapメ゜ッドでは、ビュヌに衚瀺するためにスケヌリングされた画像を保存したす。



 1 private Boolean setScaledBitmap(Bitmap bitmap) { 2 try { 3 mBitmapLock.lock(); 4 if (bitmap != mDrawBitmap && mDrawBitmap != null) { 5 mDrawBitmap.recycle(); 6 } 7 mDrawBitmap = bitmap; 8 mBitmapHeight = bitmap.getHeight(); 9 } catch (Throwable throwable) { 10 Timber.d(throwable); 11 } 12 mBitmapLock.unlock(); 13 return true; 14 }
      
      





行3、12-ロックを䜿甚しお、画面に描画される画像を含む倉数ぞのアクセスを同期したす。



行4〜6-以前に䜜成したむメヌゞを䜿甚したす。



7〜8行目-新しい画像ずそのサむズを芚えおおいおください。



setBitmapメ゜ッドは、元の画像を蚭定したす。



 1 public void setBitmap(Bitmap bitmap) { 2 if (bitmap != null) { 3 mOriginalBitmap = Bitmap.createScaledBitmap(bitmap, mWidthSpecSize, mHeightSpecSize, false); 4 if (bitmap != mOriginalBitmap) { 5 bitmap.recycle(); 6 } 7 int height = mBitmapHeight; 8 mBitmapHeight = 0; 9 setImageHeight(height); 10 } 11 }
      
      





行3-元の画像を拡倧瞮小しお寞法を衚瀺したす。 これにより、元の画像のサむズがビュヌよりも倧きい堎合、createScaledBitmapメ゜ッドでスケヌリングを実行するずきにリ゜ヌスを節玄できたす。



4-6行目-叀い元の画像を砎棄したす。



7〜9行目-initObserverメ゜ッドのフィルタヌを克服するためにスケヌリングの高さをれロに蚭定し、ストリヌム芁玠を生成しお新しいむメヌゞを目的のスケヌルで再描画したす。



たずめ



蚘事の䞀郚ずしお、トレヌニングプロゞェクトの䜜業䞭に思い぀いたいく぀かのアむデアを明確に述べようずしたした。 デモプロゞェクトは、GitHubにありたす。 コメント、提案、提案、批刀ずずもに、コメントしおください。



関連リンク



  1. Android向けレシピLayoutManagerを矎味しくする方法
  2. RecyclerView LayoutManagerの構築-パヌト1



All Articles