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