ViewPager2.java revision 04550b25e51169bb5446f73caf1fc2e00d68ef1b
1/*
2 * Copyright (C) 2017 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package androidx.viewpager2.widget;
18
19import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
20
21import static java.lang.annotation.RetentionPolicy.CLASS;
22
23import android.content.Context;
24import android.graphics.Rect;
25import android.os.Build;
26import android.os.Parcel;
27import android.os.Parcelable;
28import android.util.AttributeSet;
29import android.util.SparseArray;
30import android.view.Gravity;
31import android.view.View;
32import android.view.ViewGroup;
33import android.widget.FrameLayout;
34
35import androidx.annotation.IntDef;
36import androidx.annotation.NonNull;
37import androidx.annotation.Nullable;
38import androidx.annotation.RequiresApi;
39import androidx.annotation.RestrictTo;
40import androidx.core.view.ViewCompat;
41import androidx.fragment.app.Fragment;
42import androidx.fragment.app.FragmentManager;
43import androidx.fragment.app.FragmentPagerAdapter;
44import androidx.fragment.app.FragmentStatePagerAdapter;
45import androidx.fragment.app.FragmentTransaction;
46import androidx.recyclerview.widget.LinearLayoutManager;
47import androidx.recyclerview.widget.PagerSnapHelper;
48import androidx.recyclerview.widget.RecyclerView;
49import androidx.recyclerview.widget.RecyclerView.Adapter;
50import androidx.recyclerview.widget.RecyclerView.ViewHolder;
51
52import java.lang.annotation.Retention;
53import java.util.ArrayList;
54import java.util.List;
55
56/**
57 * Work in progress: go/viewpager2
58 *
59 * @hide
60 */
61@RestrictTo(LIBRARY_GROUP)
62public class ViewPager2 extends ViewGroup {
63    // reused in layout(...)
64    private final Rect mTmpContainerRect = new Rect();
65    private final Rect mTmpChildRect = new Rect();
66
67    private RecyclerView mRecyclerView;
68
69    public ViewPager2(Context context) {
70        super(context);
71        initialize(context);
72    }
73
74    public ViewPager2(Context context, AttributeSet attrs) {
75        this(context, attrs, 0);
76    }
77
78    public ViewPager2(Context context, AttributeSet attrs, int defStyleAttr) {
79        super(context, attrs, defStyleAttr);
80        initialize(context);
81    }
82
83    @RequiresApi(21)
84    public ViewPager2(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
85        // TODO(b/70663531): handle attrs, defStyleAttr, defStyleRes
86        super(context, attrs, defStyleAttr, defStyleRes);
87        initialize(context);
88    }
89
90    private void initialize(Context context) {
91        mRecyclerView = new RecyclerView(context);
92        mRecyclerView.setId(ViewCompat.generateViewId());
93
94        LinearLayoutManager layoutManager = new LinearLayoutManager(context);
95        // TODO(b/69103581): add support for vertical layout
96        // TODO(b/69398856): add support for RTL
97        layoutManager.setOrientation(RecyclerView.HORIZONTAL);
98        mRecyclerView.setLayoutManager(layoutManager);
99
100        mRecyclerView.setLayoutParams(
101                new ViewGroup.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
102
103        // TODO(b/70666992): add automated test for orientation change
104        new PagerSnapHelper().attachToRecyclerView(mRecyclerView);
105
106        attachViewToParent(mRecyclerView, 0, mRecyclerView.getLayoutParams());
107    }
108
109    @Nullable
110    @Override
111    protected Parcelable onSaveInstanceState() {
112        Parcelable superState = super.onSaveInstanceState();
113        SavedState ss = new SavedState(superState);
114
115        ss.mRecyclerViewId = mRecyclerView.getId();
116
117        Adapter adapter = mRecyclerView.getAdapter();
118        if (adapter instanceof FragmentStateAdapter) {
119            ss.mAdapterState = ((FragmentStateAdapter) adapter).saveState();
120        }
121
122        return ss;
123    }
124
125    @Override
126    protected void onRestoreInstanceState(Parcelable state) {
127        if (!(state instanceof SavedState)) {
128            super.onRestoreInstanceState(state);
129            return;
130        }
131
132        SavedState ss = (SavedState) state;
133        super.onRestoreInstanceState(ss.getSuperState());
134
135        if (ss.mAdapterState != null) {
136            Adapter adapter = mRecyclerView.getAdapter();
137            if (adapter instanceof FragmentStateAdapter) {
138                ((FragmentStateAdapter) adapter).restoreState(ss.mAdapterState);
139            }
140        }
141    }
142
143    @Override
144    protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) {
145        // RecyclerView changed an id, so we need to reflect that in the saved state
146        Parcelable state = container.get(getId());
147        if (state instanceof SavedState) {
148            final int previousRvId = ((SavedState) state).mRecyclerViewId;
149            final int currentRvId = mRecyclerView.getId();
150            container.put(currentRvId, container.get(previousRvId));
151            container.remove(previousRvId);
152        }
153
154        super.dispatchRestoreInstanceState(container);
155    }
156
157    static class SavedState extends BaseSavedState {
158        int mRecyclerViewId;
159        Parcelable[] mAdapterState;
160
161        @RequiresApi(24)
162        SavedState(Parcel source, ClassLoader loader) {
163            super(source, loader);
164            readValues(source, loader);
165        }
166
167        SavedState(Parcel source) {
168            super(source);
169            readValues(source, null);
170        }
171
172        SavedState(Parcelable superState) {
173            super(superState);
174        }
175
176        private void readValues(Parcel source, ClassLoader loader) {
177            mRecyclerViewId = source.readInt();
178            mAdapterState = source.readParcelableArray(loader);
179        }
180
181        @Override
182        public void writeToParcel(Parcel out, int flags) {
183            super.writeToParcel(out, flags);
184            out.writeInt(mRecyclerViewId);
185            out.writeParcelableArray(mAdapterState, flags);
186        }
187
188        static final Creator<SavedState> CREATOR = new ClassLoaderCreator<SavedState>() {
189            @Override
190            public SavedState createFromParcel(Parcel source, ClassLoader loader) {
191                return Build.VERSION.SDK_INT >= 24
192                        ? new SavedState(source, loader)
193                        : new SavedState(source);
194            }
195
196            @Override
197            public SavedState createFromParcel(Parcel source) {
198                return createFromParcel(source, null);
199            }
200
201            @Override
202            public SavedState[] newArray(int size) {
203                return new SavedState[size];
204            }
205        };
206    }
207
208    /**
209     * TODO(b/70663708): decide on an Adapter class. Here supporting RecyclerView.Adapter.
210     *
211     * @see RecyclerView#setAdapter(Adapter)
212     */
213    public <VH extends ViewHolder> void setAdapter(final Adapter<VH> adapter) {
214        mRecyclerView.setAdapter(new Adapter<VH>() {
215            private final Adapter<VH> mAdapter = adapter;
216
217            @NonNull
218            @Override
219            public VH onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
220                VH viewHolder = mAdapter.onCreateViewHolder(parent, viewType);
221
222                LayoutParams layoutParams = viewHolder.itemView.getLayoutParams();
223                if (layoutParams.width != LayoutParams.MATCH_PARENT
224                        || layoutParams.height != LayoutParams.MATCH_PARENT) {
225                    // TODO(b/70666614): decide if throw an exception or wrap in FrameLayout
226                    // ourselves; consider accepting exact size equal to parent's exact size
227                    throw new IllegalStateException(String.format(
228                            "Item's root view must fill the whole %s (use match_parent)",
229                            ViewPager2.this.getClass().getSimpleName()));
230                }
231
232                return viewHolder;
233            }
234
235            @Override
236            public void onBindViewHolder(@NonNull VH holder, int position) {
237                mAdapter.onBindViewHolder(holder, position);
238            }
239
240            @Override
241            public int getItemCount() {
242                return mAdapter.getItemCount();
243            }
244        });
245    }
246
247    /**
248     * TODO(b/70663708): decide on an Adapter class. Here supporting {@link Fragment}s.
249     *
250     * @param fragmentRetentionPolicy allows for future parameterization of Fragment memory
251     *                                strategy, similar to what {@link FragmentPagerAdapter} and
252     *                                {@link FragmentStatePagerAdapter} provide.
253     */
254    public void setAdapter(FragmentManager fragmentManager, FragmentProvider fragmentProvider,
255            @FragmentRetentionPolicy int fragmentRetentionPolicy) {
256        if (fragmentRetentionPolicy != FragmentRetentionPolicy.SAVE_STATE) {
257            throw new IllegalArgumentException("Currently only SAVE_STATE policy is supported");
258        }
259
260        mRecyclerView.setAdapter(new FragmentStateAdapter(fragmentManager, fragmentProvider));
261    }
262
263    /**
264     * Similar in behavior to {@link FragmentStatePagerAdapter}
265     * <p>
266     * Lifecycle within {@link RecyclerView}:
267     * <ul>
268     * <li>{@link RecyclerView.ViewHolder} initially an empty {@link FrameLayout}, serves as a
269     * re-usable container for a {@link Fragment} in later stages.
270     * <li>{@link RecyclerView.Adapter#onBindViewHolder} we ask for a {@link Fragment} for the
271     * position. If we already have the fragment, or have previously saved its state, we use those.
272     * <li>{@link RecyclerView.Adapter#onAttachedToWindow} we attach the {@link Fragment} to a
273     * container.
274     * <li>{@link RecyclerView.Adapter#onViewRecycled} and
275     * {@link RecyclerView.Adapter#onFailedToRecycleView} we remove, save state, destroy the
276     * {@link Fragment}.
277     * </ul>
278     */
279    private static class FragmentStateAdapter extends RecyclerView.Adapter<FragmentViewHolder> {
280        private final List<Fragment> mFragments = new ArrayList<>();
281
282        private final List<Fragment.SavedState> mSavedStates = new ArrayList<>();
283        // TODO: handle current item's menuVisibility userVisibleHint as FragmentStatePagerAdapter
284
285        private final FragmentManager mFragmentManager;
286        private final FragmentProvider mFragmentProvider;
287
288        private FragmentStateAdapter(FragmentManager fragmentManager,
289                FragmentProvider fragmentProvider) {
290            this.mFragmentManager = fragmentManager;
291            this.mFragmentProvider = fragmentProvider;
292        }
293
294        @NonNull
295        @Override
296        public FragmentViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
297            return FragmentViewHolder.create(parent);
298        }
299
300        @Override
301        public void onBindViewHolder(@NonNull FragmentViewHolder holder, int position) {
302            if (ViewCompat.isAttachedToWindow(holder.getContainer())) {
303                // this should never happen; if it does, it breaks our assumption that attaching
304                // a Fragment can reliably happen inside onViewAttachedToWindow
305                throw new IllegalStateException(
306                        String.format("View %s unexpectedly attached to a window.",
307                                holder.getContainer()));
308            }
309
310            holder.mFragment = getFragment(position);
311        }
312
313        private Fragment getFragment(int position) {
314            Fragment fragment = mFragmentProvider.getItem(position);
315            if (mSavedStates.size() > position) {
316                Fragment.SavedState savedState = mSavedStates.get(position);
317                if (savedState != null) {
318                    fragment.setInitialSavedState(savedState);
319                }
320            }
321            while (mFragments.size() <= position) {
322                mFragments.add(null);
323            }
324            mFragments.set(position, fragment);
325            return fragment;
326        }
327
328        @Override
329        public void onViewAttachedToWindow(@NonNull FragmentViewHolder holder) {
330            if (holder.mFragment.isAdded()) {
331                return;
332            }
333            mFragmentManager.beginTransaction().add(holder.getContainer().getId(),
334                    holder.mFragment).commitNowAllowingStateLoss();
335        }
336
337        @Override
338        public int getItemCount() {
339            return mFragmentProvider.getCount();
340        }
341
342        @Override
343        public void onViewRecycled(@NonNull FragmentViewHolder holder) {
344            removeFragment(holder);
345        }
346
347        @Override
348        public boolean onFailedToRecycleView(@NonNull FragmentViewHolder holder) {
349            // This happens when a ViewHolder is in a transient state (e.g. during custom
350            // animation). We don't have sufficient information on how to clear up what lead to
351            // the transient state, so we are throwing away the ViewHolder to stay on the
352            // conservative side.
353            removeFragment(holder);
354            return false; // don't recycle the view
355        }
356
357        private void removeFragment(@NonNull FragmentViewHolder holder) {
358            removeFragment(holder.mFragment, holder.getAdapterPosition());
359            holder.mFragment = null;
360        }
361
362        /**
363         * Removes a Fragment and commits the operation.
364         */
365        private void removeFragment(Fragment fragment, int position) {
366            FragmentTransaction fragmentTransaction = mFragmentManager.beginTransaction();
367            removeFragment(fragment, position, fragmentTransaction);
368            fragmentTransaction.commitNowAllowingStateLoss();
369        }
370
371        /**
372         * Adds a remove operation to the transaction, but does not commit.
373         */
374        private void removeFragment(Fragment fragment, int position,
375                @NonNull FragmentTransaction fragmentTransaction) {
376            if (fragment == null) {
377                return;
378            }
379
380            if (fragment.isAdded()) {
381                while (mSavedStates.size() <= position) {
382                    mSavedStates.add(null);
383                }
384                mSavedStates.set(position, mFragmentManager.saveFragmentInstanceState(fragment));
385            }
386
387            mFragments.set(position, null);
388            fragmentTransaction.remove(fragment);
389        }
390
391        @Nullable
392        Parcelable[] saveState() {
393            FragmentTransaction fragmentTransaction = mFragmentManager.beginTransaction();
394            for (int i = 0; i < mFragments.size(); i++) {
395                removeFragment(mFragments.get(i), i, fragmentTransaction);
396            }
397            fragmentTransaction.commitNowAllowingStateLoss();
398            return mSavedStates.toArray(new Fragment.SavedState[mSavedStates.size()]);
399        }
400
401        void restoreState(@NonNull Parcelable[] savedStates) {
402            for (Parcelable savedState : savedStates) {
403                mSavedStates.add((Fragment.SavedState) savedState);
404            }
405        }
406    }
407
408    private static class FragmentViewHolder extends RecyclerView.ViewHolder {
409        private Fragment mFragment;
410
411        private FragmentViewHolder(FrameLayout container) {
412            super(container);
413        }
414
415        static FragmentViewHolder create(ViewGroup parent) {
416            FrameLayout container = new FrameLayout(parent.getContext());
417            container.setLayoutParams(
418                    new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
419                            ViewGroup.LayoutParams.MATCH_PARENT));
420            container.setId(ViewCompat.generateViewId());
421            return new FragmentViewHolder(container);
422        }
423
424        FrameLayout getContainer() {
425            return (FrameLayout) itemView;
426        }
427    }
428
429    /**
430     * Provides {@link Fragment}s for pages
431     */
432    public interface FragmentProvider {
433        /**
434         * Return the Fragment associated with a specified position.
435         */
436        Fragment getItem(int position);
437
438        /**
439         * Return the number of pages available.
440         */
441        int getCount();
442    }
443
444    @Retention(CLASS)
445    @IntDef({FragmentRetentionPolicy.SAVE_STATE})
446    public @interface FragmentRetentionPolicy {
447        /** Approach similar to {@link FragmentStatePagerAdapter} */
448        int SAVE_STATE = 0;
449    }
450
451    @Override
452    public void onViewAdded(View child) {
453        // TODO(b/70666620): consider adding a support for Decor views
454        throw new IllegalStateException(
455                getClass().getSimpleName() + " does not support direct child views");
456    }
457
458    @Override
459    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
460        // TODO(b/70666622): consider margin support
461        // TODO(b/70666626): consider delegating all this to RecyclerView
462        // TODO(b/70666625): write automated tests for this
463
464        measureChild(mRecyclerView, widthMeasureSpec, heightMeasureSpec);
465        int width = mRecyclerView.getMeasuredWidth();
466        int height = mRecyclerView.getMeasuredHeight();
467        int childState = mRecyclerView.getMeasuredState();
468
469        width += getPaddingLeft() + getPaddingRight();
470        height += getPaddingTop() + getPaddingBottom();
471
472        width = Math.max(width, getSuggestedMinimumWidth());
473        height = Math.max(height, getSuggestedMinimumHeight());
474
475        setMeasuredDimension(resolveSizeAndState(width, widthMeasureSpec, childState),
476                resolveSizeAndState(height, heightMeasureSpec,
477                        childState << MEASURED_HEIGHT_STATE_SHIFT));
478    }
479
480    @Override
481    protected void onLayout(boolean changed, int l, int t, int r, int b) {
482        int width = mRecyclerView.getMeasuredWidth();
483        int height = mRecyclerView.getMeasuredHeight();
484
485        // TODO(b/70666626): consider delegating padding handling to the RecyclerView to avoid
486        // an unnatural page transition effect: http://shortn/_Vnug3yZpQT
487        mTmpContainerRect.left = getPaddingLeft();
488        mTmpContainerRect.right = r - l - getPaddingRight();
489        mTmpContainerRect.top = getPaddingTop();
490        mTmpContainerRect.bottom = b - t - getPaddingBottom();
491
492        Gravity.apply(Gravity.TOP | Gravity.START, width, height, mTmpContainerRect, mTmpChildRect);
493        mRecyclerView.layout(mTmpChildRect.left, mTmpChildRect.top, mTmpChildRect.right,
494                mTmpChildRect.bottom);
495    }
496}
497