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