ViewPager2.java revision ac5fe7c617c66850fff75a9fce9979c6e5674b0f
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 androidx.annotation.IntDef;
26import androidx.annotation.NonNull;
27import androidx.annotation.RequiresApi;
28import androidx.annotation.RestrictTo;
29import androidx.fragment.app.Fragment;
30import androidx.fragment.app.FragmentManager;
31import androidx.fragment.app.FragmentPagerAdapter;
32import androidx.fragment.app.FragmentStatePagerAdapter;
33import androidx.core.view.ViewCompat;
34import androidx.recyclerview.widget.LinearLayoutManager;
35import androidx.recyclerview.widget.PagerSnapHelper;
36import androidx.recyclerview.widget.RecyclerView;
37import androidx.recyclerview.widget.RecyclerView.Adapter;
38import androidx.recyclerview.widget.RecyclerView.ViewHolder;
39import android.util.AttributeSet;
40import android.view.Gravity;
41import android.view.View;
42import android.view.ViewGroup;
43import android.widget.FrameLayout;
44
45import java.lang.annotation.Retention;
46import java.util.ArrayList;
47import java.util.List;
48
49/**
50 * Work in progress: go/viewpager2
51 *
52 * @hide
53 */
54@RestrictTo(LIBRARY_GROUP)
55public class ViewPager2 extends ViewGroup {
56    // reused in layout(...)
57    private final Rect mTmpContainerRect = new Rect();
58    private final Rect mTmpChildRect = new Rect();
59
60    private RecyclerView mRecyclerView;
61
62    public ViewPager2(Context context) {
63        super(context);
64        initialize(context);
65    }
66
67    public ViewPager2(Context context, AttributeSet attrs) {
68        this(context, attrs, 0);
69    }
70
71    public ViewPager2(Context context, AttributeSet attrs, int defStyleAttr) {
72        super(context, attrs, defStyleAttr);
73        initialize(context);
74    }
75
76    @RequiresApi(21)
77    public ViewPager2(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
78        // TODO(b/70663531): handle attrs, defStyleAttr, defStyleRes
79        super(context, attrs, defStyleAttr, defStyleRes);
80        initialize(context);
81    }
82
83    private void initialize(Context context) {
84        mRecyclerView = new RecyclerView(context);
85
86        LinearLayoutManager layoutManager = new LinearLayoutManager(context);
87        // TODO(b/69103581): add support for vertical layout
88        // TODO(b/69398856): add support for RTL
89        layoutManager.setOrientation(RecyclerView.HORIZONTAL);
90        mRecyclerView.setLayoutManager(layoutManager);
91
92        mRecyclerView.setLayoutParams(
93                new ViewGroup.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
94
95        // TODO(b/70666992): add automated test for orientation change
96        new PagerSnapHelper().attachToRecyclerView(mRecyclerView);
97
98        attachViewToParent(mRecyclerView, 0, mRecyclerView.getLayoutParams());
99    }
100
101    /**
102     * TODO(b/70663708): decide on an Adapter class. Here supporting RecyclerView.Adapter.
103     *
104     * @see RecyclerView#setAdapter(Adapter)
105     */
106    public <VH extends ViewHolder> void setAdapter(final Adapter<VH> adapter) {
107        mRecyclerView.setAdapter(new Adapter<VH>() {
108            private final Adapter<VH> mAdapter = adapter;
109
110            @NonNull
111            @Override
112            public VH onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
113                VH viewHolder = mAdapter.onCreateViewHolder(parent, viewType);
114
115                LayoutParams layoutParams = viewHolder.itemView.getLayoutParams();
116                if (layoutParams.width != LayoutParams.MATCH_PARENT
117                        || layoutParams.height != LayoutParams.MATCH_PARENT) {
118                    // TODO(b/70666614): decide if throw an exception or wrap in FrameLayout
119                    // ourselves; consider accepting exact size equal to parent's exact size
120                    throw new IllegalStateException(String.format(
121                            "Item's root view must fill the whole %s (use match_parent)",
122                            ViewPager2.this.getClass().getSimpleName()));
123                }
124
125                return viewHolder;
126            }
127
128            @Override
129            public void onBindViewHolder(@NonNull VH holder, int position) {
130                mAdapter.onBindViewHolder(holder, position);
131            }
132
133            @Override
134            public int getItemCount() {
135                return mAdapter.getItemCount();
136            }
137        });
138    }
139
140    /**
141     * TODO(b/70663708): decide on an Adapter class. Here supporting {@link Fragment}s.
142     *
143     * @param fragmentRetentionPolicy allows for future parameterization of Fragment memory
144     *                                strategy, similar to what {@link FragmentPagerAdapter} and
145     *                                {@link FragmentStatePagerAdapter} provide.
146     */
147    public void setAdapter(FragmentManager fragmentManager, FragmentProvider fragmentProvider,
148            @FragmentRetentionPolicy int fragmentRetentionPolicy) {
149        if (fragmentRetentionPolicy != FragmentRetentionPolicy.SAVE_STATE) {
150            throw new IllegalArgumentException("Currently only SAVE_STATE policy is supported");
151        }
152
153        mRecyclerView.setAdapter(new FragmentStateAdapter(fragmentManager, fragmentProvider));
154    }
155
156    /**
157     * Similar in behavior to {@link FragmentStatePagerAdapter}
158     * <p>
159     * Lifecycle within {@link RecyclerView}:
160     * <ul>
161     * <li>{@link RecyclerView.ViewHolder} initially an empty {@link FrameLayout}, serves as a
162     * re-usable container for a {@link Fragment} in later stages.
163     * <li>{@link RecyclerView.Adapter#onBindViewHolder} we ask for a {@link Fragment} for the
164     * position. If we already have the fragment, or have previously saved its state, we use those.
165     * <li>{@link RecyclerView.Adapter#onAttachedToWindow} we attach the {@link Fragment} to a
166     * container.
167     * <li>{@link RecyclerView.Adapter#onViewRecycled} and
168     * {@link RecyclerView.Adapter#onFailedToRecycleView} we remove, save state, destroy the
169     * {@link Fragment}.
170     * </ul>
171     */
172    private static class FragmentStateAdapter extends RecyclerView.Adapter<FragmentViewHolder> {
173        private final List<Fragment.SavedState> mSavedStates = new ArrayList<>();
174        // TODO: handle current item's menuVisibility userVisibleHint as FragmentStatePagerAdapter
175
176        private final FragmentManager mFragmentManager;
177        private final FragmentProvider mFragmentProvider;
178
179        private FragmentStateAdapter(FragmentManager fragmentManager,
180                FragmentProvider fragmentProvider) {
181            this.mFragmentManager = fragmentManager;
182            this.mFragmentProvider = fragmentProvider;
183        }
184
185        @NonNull
186        @Override
187        public FragmentViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
188            return FragmentViewHolder.create(parent);
189        }
190
191        @Override
192        public void onBindViewHolder(@NonNull FragmentViewHolder holder, int position) {
193            if (ViewCompat.isAttachedToWindow(holder.getContainer())) {
194                // this should never happen; if it does, it breaks our assumption that attaching
195                // a Fragment can reliably happen inside onViewAttachedToWindow
196                throw new IllegalStateException(
197                        String.format("View %s unexpectedly attached to a window.",
198                                holder.getContainer()));
199            }
200
201            holder.mFragment = getFragment(position);
202        }
203
204        private Fragment getFragment(int position) {
205            Fragment fragment = mFragmentProvider.getItem(position);
206            if (mSavedStates.size() > position) {
207                Fragment.SavedState savedState = mSavedStates.get(position);
208                if (savedState != null) {
209                    fragment.setInitialSavedState(savedState);
210                }
211            }
212            return fragment;
213        }
214
215        @Override
216        public void onViewAttachedToWindow(@NonNull FragmentViewHolder holder) {
217            if (holder.mFragment.isAdded()) {
218                return;
219            }
220            mFragmentManager.beginTransaction().add(holder.getContainer().getId(),
221                    holder.mFragment).commitNowAllowingStateLoss();
222        }
223
224        @Override
225        public int getItemCount() {
226            return mFragmentProvider.getCount();
227        }
228
229        @Override
230        public void onViewRecycled(@NonNull FragmentViewHolder holder) {
231            removeFragment(holder);
232        }
233
234        @Override
235        public boolean onFailedToRecycleView(@NonNull FragmentViewHolder holder) {
236            // This happens when a ViewHolder is in a transient state (e.g. during custom
237            // animation). We don't have sufficient information on how to clear up what lead to
238            // the transient state, so we are throwing away the ViewHolder to stay on the
239            // conservative side.
240            removeFragment(holder);
241            return false; // don't recycle the view
242        }
243
244        private void removeFragment(@NonNull FragmentViewHolder holder) {
245            if (holder.mFragment == null) {
246                return; // fresh ViewHolder, nothing to do
247            }
248
249            int position = holder.getAdapterPosition();
250
251            if (holder.mFragment.isAdded()) {
252                while (mSavedStates.size() <= position) {
253                    mSavedStates.add(null);
254                }
255                mSavedStates.set(position,
256                        mFragmentManager.saveFragmentInstanceState(holder.mFragment));
257            }
258
259            mFragmentManager.beginTransaction().remove(
260                    holder.mFragment).commitNowAllowingStateLoss();
261            holder.mFragment = null;
262        }
263    }
264
265    private static class FragmentViewHolder extends RecyclerView.ViewHolder {
266        private Fragment mFragment;
267
268        private FragmentViewHolder(FrameLayout container) {
269            super(container);
270        }
271
272        static FragmentViewHolder create(ViewGroup parent) {
273            FrameLayout container = new FrameLayout(parent.getContext());
274            container.setLayoutParams(
275                    new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
276                            ViewGroup.LayoutParams.MATCH_PARENT));
277            container.setId(ViewCompat.generateViewId());
278            return new FragmentViewHolder(container);
279        }
280
281        FrameLayout getContainer() {
282            return (FrameLayout) itemView;
283        }
284    }
285
286    /**
287     * Provides {@link Fragment}s for pages
288     */
289    public interface FragmentProvider {
290        /**
291         * Return the Fragment associated with a specified position.
292         */
293        Fragment getItem(int position);
294
295        /**
296         * Return the number of pages available.
297         */
298        int getCount();
299    }
300
301    @Retention(CLASS)
302    @IntDef({FragmentRetentionPolicy.SAVE_STATE})
303    public @interface FragmentRetentionPolicy {
304        /** Approach similar to {@link FragmentStatePagerAdapter} */
305        int SAVE_STATE = 0;
306    }
307
308    @Override
309    public void onViewAdded(View child) {
310        // TODO(b/70666620): consider adding a support for Decor views
311        throw new IllegalStateException(
312                getClass().getSimpleName() + " does not support direct child views");
313    }
314
315    /** @see RecyclerView#addOnScrollListener(RecyclerView.OnScrollListener) */
316    public void addOnScrollListener(RecyclerView.OnScrollListener listener) {
317        mRecyclerView.addOnScrollListener(listener);
318    }
319
320    @Override
321    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
322        // TODO(b/70666622): consider margin support
323        // TODO(b/70666626): consider delegating all this to RecyclerView
324        // TODO(b/70666625): write automated tests for this
325
326        measureChild(mRecyclerView, widthMeasureSpec, heightMeasureSpec);
327        int width = mRecyclerView.getMeasuredWidth();
328        int height = mRecyclerView.getMeasuredHeight();
329        int childState = mRecyclerView.getMeasuredState();
330
331        width += getPaddingLeft() + getPaddingRight();
332        height += getPaddingTop() + getPaddingBottom();
333
334        width = Math.max(width, getSuggestedMinimumWidth());
335        height = Math.max(height, getSuggestedMinimumHeight());
336
337        setMeasuredDimension(resolveSizeAndState(width, widthMeasureSpec, childState),
338                resolveSizeAndState(height, heightMeasureSpec,
339                        childState << MEASURED_HEIGHT_STATE_SHIFT));
340    }
341
342    @Override
343    protected void onLayout(boolean changed, int l, int t, int r, int b) {
344        int width = mRecyclerView.getMeasuredWidth();
345        int height = mRecyclerView.getMeasuredHeight();
346
347        // TODO(b/70666626): consider delegating padding handling to the RecyclerView to avoid
348        // an unnatural page transition effect: http://shortn/_Vnug3yZpQT
349        mTmpContainerRect.left = getPaddingLeft();
350        mTmpContainerRect.right = r - l - getPaddingRight();
351        mTmpContainerRect.top = getPaddingTop();
352        mTmpContainerRect.bottom = b - t - getPaddingBottom();
353
354        Gravity.apply(Gravity.TOP | Gravity.START, width, height, mTmpContainerRect, mTmpChildRect);
355        mRecyclerView.layout(mTmpChildRect.left, mTmpChildRect.top, mTmpChildRect.right,
356                mTmpChildRect.bottom);
357    }
358}
359