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