1 はじめに
前章[Android フレームワーク シリーズ] 第 14 章 Fragment の基本原則 (AndroidX 版)では Fragment の基本原則を学びましたが、この章では、一般的に使用される用途Fragment+ViewPager
とFragment+ViewPager2
関連する用途、および基本的なソース コード分析について学びます。
2 フラグメント+ViewPager
私たちは一般的に と という 2 つの実装クラスを使用しますがPagerAdapter
、今日はそれらの使用方法と比較方法を学びます。FragmentStatePagerAdapter
FragmentPagerAdapter
2.1 FragmentPagerAdapter と FragmentStatePagerAdapter の違い
1. fragments
オブジェクトの処理:
FragmentPagerAdapter
: スコープ外はfragments
メモリ ( ) に保存されますdetach
が、fragment
対応するオブジェクトはView
破棄されます
FragmentStatePagerAdapter
。 : スコープ外はメモリ ( )fragments
に保存されず、同様に破棄されます。2. ステータス処理: :範囲外の対応するものを保存します。 :範囲内の対応するもののみを保存します。これは、ライフサイクル コールバックで渡される外部パラメーターに使用され、同様のものです。3. 適用可能なシナリオ: 同じ量、メモリは大きくなりますが、ページの切り替えはよりフレンドリーです。メモリの使用量は少なく、ページの切り替えはわずかに悪くなります。remove
View
FragmentPagerAdapter
fragments
SavedState
FragmentStatePagerAdapter
fragments
SavedState
SavedState
Fragment
Activity
fragments
FragmentPagerAdapter
FragmentStatePagerAdapter
したがって、数が少ない状況FragmentPagerAdapter
に適しており、 Fragment の数が多い状況にも適しています。Fragment
FragmentStatePagerAdapter
まずは見てみましょうFragmentPagerAdapter
:
@Override
public Object instantiateItem(ViewGroup container, int position) {
if (mCurTransaction == null) {
mCurTransaction = mFragmentManager.beginTransaction();
}
final long itemId = getItemId(position);
// Do we already have this fragment?
//判断请求的Fragment是否已经被生成过
String name = makeFragmentName(container.getId(), itemId);
Fragment fragment = mFragmentManager.findFragmentByTag(name);
if (fragment != null) {
if (DEBUG) Log.v(TAG, "Attaching item #" + itemId + ": f=" + fragment);
// 当前缓存有则直接使用
mCurTransaction.attach(fragment);
} else {
fragment = getItem(position);
//调用这个方法来生成新的Fragment
if (DEBUG) Log.v(TAG, "Adding item #" + itemId + ": f=" + fragment);
// 将新生成的Fragment存储起来,以便以后再次用到时,直接attach()
mCurTransaction.add(container.getId(), fragment,
makeFragmentName(container.getId(), itemId));
}
if (fragment != mCurrentPrimaryItem) {
fragment.setMenuVisibility(false);
if (mBehavior == BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
mCurTransaction.setMaxLifecycle(fragment, Lifecycle.State.STARTED);
} else {
fragment.setUserVisibleHint(false);
}
}
return fragment;
}
@Override
public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
Fragment fragment = (Fragment) object;
if (mCurTransaction == null) {
mCurTransaction = mFragmentManager.beginTransaction();
}
if (DEBUG) Log.v(TAG, "Detaching item #" + getItemId(position) + ": f=" + object
+ " v=" + fragment.getView());
mCurTransaction.detach(fragment);
if (fragment.equals(mCurrentPrimaryItem)) {
mCurrentPrimaryItem = null;
}
}
FragmentPagerAdapter
destroyItem
呼び出されるメソッドはフラグメントdetach()
の状態を変更するだけです。つまり、adapter
時間全体を削除することによってのみ、生成されたすべてのフラグメントをFragment
削除できます。そうでない場合、フラグメントはメモリ内に直接存在します。
次に比較しますFragmentStatePagerAdapter
:
@Override
public Object instantiateItem(@NonNull ViewGroup container, int position) {
// If we already have this item instantiated, there is nothing
// to do. This can happen when we are restoring the entire pager
// from its saved state, where the fragment manager has already
// taken care of restoring the fragments we previously had instantiated.
// mFragments中对应位置有Fragment的情况下直接返回
if (mFragments.size() > position) {
Fragment f = mFragments.get(position);
if (f != null) {
return f;
}
}
if (mCurTransaction == null) {
mCurTransaction = mFragmentManager.beginTransaction();
}
Fragment fragment = getItem(position);
if (DEBUG) Log.v(TAG, "Adding item #" + position + ": f=" + fragment);
if (mSavedState.size() > position) {
Fragment.SavedState fss = mSavedState.get(position);
if (fss != null) {
fragment.setInitialSavedState(fss);
}
}
while (mFragments.size() <= position) {
mFragments.add(null);
}
fragment.setMenuVisibility(false);
if (mBehavior == BEHAVIOR_SET_USER_VISIBLE_HINT) {
fragment.setUserVisibleHint(false);
}
mFragments.set(position, fragment);
mCurTransaction.add(container.getId(), fragment);
if (mBehavior == BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
mCurTransaction.setMaxLifecycle(fragment, Lifecycle.State.STARTED);
}
return fragment;
}
@Override
public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
Fragment fragment = (Fragment) object;
if (mCurTransaction == null) {
mCurTransaction = mFragmentManager.beginTransaction();
}
if (DEBUG) Log.v(TAG, "Removing item #" + position + ": f=" + object
+ " v=" + ((Fragment)object).getView());
while (mSavedState.size() <= position) {
mSavedState.add(null);
}
mSavedState.set(position, fragment.isAdded()
? mFragmentManager.saveFragmentInstanceState(fragment) : null);
mFragments.set(position, null);
// 缓存中移除fragment,下次使用得重新创建
mCurTransaction.remove(fragment);
if (fragment.equals(mCurrentPrimaryItem)) {
mCurrentPrimaryItem = null;
}
}
FragmentStatePagerAdapter
destroyItem
メソッドが呼び出されたときに削除されたFragment
ため、次回再作成する必要があります。Fragment
3 フラグメント+ViewPager2
ViewPager2
これはAndroid Jetpack
ライブラリ内のコンポーネントであり、アプリケーションでページ切り替えやスライド効果を実装するために使用されるコンテナです。実際、ViewPager2
それ自体は から継承されていますRecyclerView
。[Android フレームワーク シリーズ] 第 12 章 RecycleView 関連の原則と 4 レベルのキャッシュ戦略分析を確認できます。
3.1 ViewPager2の役割と目的
3.1.1 ViewPager2 の使用シナリオ
さまざまなシナリオで使用できる強力なスライディング コンテナです。柔軟なページ切り替え機能とレイアウトのカスタマイズ機能を提供し、アプリケーション インターフェイスをよりリッチでインタラクティブにします。次のシナリオで使用できます。
-
オンボーディング ページまたはウェルカム ページの実装
ViewPager2
を使用して、ユーザーがスワイプしてアプリの機能を紹介したり、ウェルカム コンテンツを表示したりできるオンボーディング ページまたはウェルカム ページを作成できます。 -
[画像ブラウザの作成]
ViewPager2
を使用して画像ブラウザを作成すると、ユーザーはスライドしてさまざまな画像を切り替えることができ、ズームとジェスチャの操作がサポートされます。 -
カルーセルの構築は
ViewPager2
、アダプタを通じてさまざまなカルーセル項目を動的にロードし、自動サイクル スクロールを提供できるカルーセル関数の構築に非常に適しています。 -
タブ付きレイアウトの実装
組み合わせを使用してタブ付きレイアウトを作成でき、ユーザーはタブをスライドさせることで異なるコンテンツ ページ間を切り替えることができますTabLayout
。ViewPager2
-
縦スライド ページの作成
とは異なりViewPager
、ViewPager2
縦スライドに対応しているため、縦スライド ナビゲーション メニューや縦ニュース リストなど、縦スライド ページ レイアウトの作成に使用できます。 -
ページング データ表示の実装を
ViewPager2
使用すると、ページごとに大量のデータをロードし、各ページにコンテンツの一部を表示するなど、ページング データを表示できます。 -
ネストされたスライディング レイアウトは、
ViewPager2
他のスライディング コンポーネント ( などRecyclerView
) とネストして使用して、複雑なスライディング レイアウト構造を実装できます。 -
カスタマイズされたスライド効果の実現
カスタマイズされたコンバーター (Transformer
) を使用すると、グラデーション、ズーム、回転など、さまざまなクールなページ切り替え効果を実現できます。
3.1.2 ViewPager と比較した ViewPager2 の改善点と利点
ViewPager2
ViewPager
はオリジナルの改良バージョンであり、より優れたパフォーマンス、より柔軟なアダプタ、およびより豊富な機能を提供します。これは、スライディング ページ レイアウトを構築するための推奨コンポーネントです。アプリケーションでさまざまなスライディング ページ要件を実装し、より良いユーザー エクスペリエンスを提供できます。次のような改善点と利点があります。
-
垂直スライドのサポートが
ViewPager2
改良ViewPager
されており、最も重要な改良点の 1 つは です支持垂直滑动
。AndroidではViewPager
横スライドのみ対応しています。これにより、ViewPager2
垂直レイアウトや特定のシナリオを作成する際に、垂直スライド機能がより便利かつ柔軟になります。 -
パフォーマンスと安定性の向上
ViewPager2
内部実装はRecyclerView
コンテナとして使用され、ViewPager
実装に依存しなくなります。これにより、次ViewPager2
のようなRecyclerView
利点が得られます。同時に、誤ったエントリ位置やタイミングの悪いデータ更新など、いくつかの既知の問題や不安定性も解決されました。更好的性能和内存管理
更流畅的滑动体验
及更好的布局回收和复用机制
ViewPager2
ViewPager
-
ページとしての Fragment の使用をサポートします。 Fragment
とは異なり、渡したり適応したりせずにViewPager
、Fragment のページとしてのViewPager2
使用を直接サポートします。これにより、ページ管理とライフサイクル処理が簡素化され、より直感的で一貫した使用エクスペリエンスが提供されます。Fragment
FragmentPagerAdapter
FragmentStatePagerAdapter
-
より柔軟なアダプターには、
ViewPager2
新しいアダプター インターフェイス、RecyclerView.Adapter
のサブクラスが導入されていますRecyclerView.Adapter
。これにより、アダプターの作成と管理がより柔軟になり、さらに多くの機能と拡張性が提供されます。 -
より豊富な機能とインターフェイス
ViewPager2
により、ページのプリロードのサポート、より強力なページ切り替えアニメーションのサポート、より充実したコールバック インターフェイスなど、多くの新しい機能とインターフェイスが提供されます。これらの機能とインターフェイスにより、開発者はViewPager2
動作と外観をより詳細に制御およびカスタマイズできるようになります。
3.2 フラグメント状態アダプター
ViewPager2
上記でいくつかの違いと最適化の比較ポイントについて学びました。引き続き、対応するものViewPager
を見てみましょう。それが から継承することがわかっているため、それに対応するものはから継承する必要があり、友人はそれを理解できるはずです。次に、追加する各結合はコンテナーとして表示されます。再利用に焦点を当てていますが、 にあるものは再利用されない、つまり、あるだけ作成する必要があるため、これらをどのように変換するか?ViewPager2
Adapter
ViewPager2
RecyclerView
FragmentStateAdapter
RecyclerView.Adapter
RecyclerView
Item
Fragment
RecyclerView.Adapter
ViewHolder
FragmentStateAdapter
Framgent
item
Fragment
まずソースコードを見てみましょうFragmentStateAdapter
:
public abstract class FragmentStateAdapter extends
RecyclerView.Adapter<FragmentViewHolder> implements StatefulAdapter {
// 通过FragmentStateAdapter声明中的泛型可以知道,
// ViewPager2之所以能够在RecyclerView的基础上能对外屏蔽对ViewHolder的使用,
// 其内部是借助FragmentViewHolder实现的,其内部就new了一个FrameLayout。
@Override
public final FragmentViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
return FragmentViewHolder.create(parent);
}
@Override
public final void onBindViewHolder(final @NonNull FragmentViewHolder holder, int position) {
final long itemId = holder.getItemId();
final int viewHolderId = holder.getContainer().getId();
final Long boundItemId = itemForViewHolder(viewHolderId); // item currently bound to the VH
if (boundItemId != null && boundItemId != itemId) {
removeFragment(boundItemId);
mItemIdToViewHolder.remove(boundItemId);
}
mItemIdToViewHolder.put(itemId, viewHolderId); // this might overwrite an existing entry
// 内部会最终回调到createFragment用来创建当前Fragment
ensureFragment(position);
/** Special case when {@link RecyclerView} decides to keep the {@link container}
* attached to the window, but not to the view hierarchy (i.e. parent is null) */
final FrameLayout container = holder.getContainer();
if (ViewCompat.isAttachedToWindow(container)) {
if (container.getParent() != null) {
throw new IllegalStateException("Design assumption violated.");
}
container.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
@Override
public void onLayoutChange(View v, int left, int top, int right, int bottom,
int oldLeft, int oldTop, int oldRight, int oldBottom) {
if (container.getParent() != null) {
container.removeOnLayoutChangeListener(this);
placeFragmentInViewHolder(holder);
}
}
});
}
// 回收已经不在item集合中的Fragment,节省内存开销
gcFragments();
}
private void ensureFragment(int position) {
long itemId = getItemId(position);
if (!mFragments.containsKey(itemId)) {
// TODO(133419201): check if a Fragment provided here is a new Fragment
Fragment newFragment = createFragment(position);
newFragment.setInitialSavedState(mSavedStates.get(itemId));
mFragments.put(itemId, newFragment);
}
}
void gcFragments() {
if (!mHasStaleFragments || shouldDelayFragmentTransactions()) {
return;
}
// Remove Fragments for items that are no longer part of the data-set
Set<Long> toRemove = new ArraySet<>();
for (int ix = 0; ix < mFragments.size(); ix++) {
long itemId = mFragments.keyAt(ix);
if (!containsItem(itemId)) {
toRemove.add(itemId);
mItemIdToViewHolder.remove(itemId); // in case they're still bound
}
}
// Remove Fragments that are not bound anywhere -- pending a grace period
if (!mIsInGracePeriod) {
mHasStaleFragments = false; // we've executed all GC checks
for (int ix = 0; ix < mFragments.size(); ix++) {
long itemId = mFragments.keyAt(ix);
if (!isFragmentViewBound(itemId)) {
toRemove.add(itemId);
}
}
}
for (Long itemId : toRemove) {
removeFragment(itemId);
}
}
// onViewAttachToWindow的时候调用placeFragmentInViewHolder,
// 将FragmentViewHolder的container与当前Fragment绑定
@Override
public final void onViewAttachedToWindow(@NonNull final FragmentViewHolder holder) {
placeFragmentInViewHolder(holder);
gcFragments();
}
void placeFragmentInViewHolder(@NonNull final FragmentViewHolder holder) {
Fragment fragment = mFragments.get(holder.getItemId());
if (fragment == null) {
throw new IllegalStateException("Design assumption violated.");
}
FrameLayout container = holder.getContainer();
View view = fragment.getView();
if (!fragment.isAdded() && view != null) {
throw new IllegalStateException("Design assumption violated.");
}
// { f:added, v:notCreated, v:notAttached} -> schedule callback for when created
if (fragment.isAdded() && view == null) {
scheduleViewAttach(fragment, container);
return;
}
// { f:added, v:created, v:attached } -> check if attached to the right container
if (fragment.isAdded() && view.getParent() != null) {
if (view.getParent() != container) {
addViewToContainer(view, container);
}
return;
}
// { f:added, v:created, v:notAttached} -> attach view to container
if (fragment.isAdded()) {
addViewToContainer(view, container);
return;
}
// { f:notAdded, v:notCreated, v:notAttached } -> add, create, attach
if (!shouldDelayFragmentTransactions()) {
scheduleViewAttach(fragment, container);
mFragmentManager.beginTransaction()
.add(fragment, "f" + holder.getItemId())
.setMaxLifecycle(fragment, STARTED)
.commitNow();
mFragmentMaxLifecycleEnforcer.updateFragmentMaxLifecycle(false);
} else {
if (mFragmentManager.isDestroyed()) {
return; // nothing we can do
}
mLifecycle.addObserver(new LifecycleEventObserver() {
@Override
public void onStateChanged(@NonNull LifecycleOwner source,
@NonNull Lifecycle.Event event) {
if (shouldDelayFragmentTransactions()) {
return;
}
source.getLifecycle().removeObserver(this);
if (ViewCompat.isAttachedToWindow(holder.getContainer())) {
placeFragmentInViewHolder(holder);
}
}
});
}
}
}
public final class FragmentViewHolder extends ViewHolder {
private FragmentViewHolder(@NonNull FrameLayout container) {
super(container);
}
// FragmentViewHolder实际上就创建了一个FrameLayout
@NonNull static FragmentViewHolder create(@NonNull ViewGroup parent) {
FrameLayout container = new FrameLayout(parent.getContext());
container.setLayoutParams(
new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT));
container.setId(ViewCompat.generateViewId());
container.setSaveEnabled(false);
return new FragmentViewHolder(container);
}
@NonNull FrameLayout getContainer() {
return (FrameLayout) itemView;
}
}
Fragment
上記のソースコード解析から、再利用ではないものの、再利用ViewHolder
によって代替表示container
が実現されていることがわかります。Framgent
4 まとめ
この章の内容を要約すると、次のようになります。
Fragment+ViewPager
水平方向にしかスクロールできず、パフォーマンスは比較的悪いです。対応するAdapter
エフェクトも異なります。Fragment+ViewPager
を使用するとFragmentPagerAdapter
、スコープ外はfragments
メモリ ( ) に保存されますdetach
が、fragment
該当するものはView
破棄され、fragments
該当するものはSavedState
保存されます。FragmentPagerAdapter
メモリは大きくなりますが、ページの切り替えはよりフレンドリーで、Fragment
少量に適しています。Fragment+ViewPager
使用するとFragmentStatePagerAdapter
、スコープ外のfragments
メモリ ( ) には保存されず、破棄されます。範囲内の対応するものだけが保存されます。これは、ライフサイクル コールバックで渡される外部パラメーターに使用され、同様のものです。メモリ使用量が少なく、ページ切り替えが若干悪いです。大量の場合に適しています。remove
View
fragments
SavedState
SavedState
Fragment
Activity
Fragment
Fragment+ViewPager2
水平方向または垂直方向にスクロールでき、RecyclerView
メモリ使用量、キャッシュ管理などを含む基本的にすべての利点を継承します。FragmentStateAdapter
から継承されますRecyclerView.Adapter
が、Fragment
再利用されませんが、代替表示は再利用ViewHolder
によってcontainer
実装されます。Framgent