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