Home | History | Annotate | Download | only in widget
      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 
     72     public ViewPager2(Context context) {
     73         super(context);
     74         initialize(context, null);
     75     }
     76 
     77     public ViewPager2(Context context, AttributeSet attrs) {
     78         super(context, attrs);
     79         initialize(context, attrs);
     80     }
     81 
     82     public ViewPager2(Context context, AttributeSet attrs, int defStyleAttr) {
     83         super(context, attrs, defStyleAttr);
     84         initialize(context, attrs);
     85     }
     86 
     87     @RequiresApi(21)
     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 
     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 
    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
    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
    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
    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)
    174         SavedState(Parcel source, ClassLoader loader) {
    175             super(source, loader);
    176             readValues(source, loader);
    177         }
    178 
    179         SavedState(Parcel source) {
    180             super(source);
    181             readValues(source, null);
    182         }
    183 
    184         SavedState(Parcelable superState) {
    185             super(superState);
    186         }
    187 
    188         private void readValues(Parcel source, ClassLoader loader) {
    189             mRecyclerViewId = source.readInt();
    190             mAdapterState = source.readParcelableArray(loader);
    191         }
    192 
    193         @Override
    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      */
    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      */
    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 
    300         private FragmentStateAdapter(FragmentManager fragmentManager,
    301                 FragmentProvider fragmentProvider) {
    302             this.mFragmentManager = fragmentManager;
    303             this.mFragmentProvider = fragmentProvider;
    304         }
    305 
    306         @NonNull
    307         @Override
    308         public FragmentViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
    309             return FragmentViewHolder.create(parent);
    310         }
    311 
    312         @Override
    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 
    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
    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
    350         public int getItemCount() {
    351             return mFragmentProvider.getCount();
    352         }
    353 
    354         @Override
    355         public void onViewRecycled(@NonNull FragmentViewHolder holder) {
    356             removeFragment(holder);
    357         }
    358 
    359         @Override
    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 
    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          */
    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          */
    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
    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 
    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 
    423         private FragmentViewHolder(FrameLayout container) {
    424             super(container);
    425         }
    426 
    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 
    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          */
    448         Fragment getItem(int position);
    449 
    450         /**
    451          * Return the number of pages available.
    452          */
    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
    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
    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
    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      */
    519     public void setOrientation(@Orientation int orientation) {
    520         mLayoutManager.setOrientation(orientation);
    521     }
    522 
    523     public @Orientation int getOrientation() {
    524         return mLayoutManager.getOrientation();
    525     }
    526 }
    527