1 // Copyright 2020 The Chromium Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 package org.chromium.components.browser_ui.widget.image_tiles; 6 7 import android.content.Context; 8 import android.content.res.Configuration; 9 import android.graphics.Rect; 10 import android.view.View; 11 import android.view.animation.Animation; 12 import android.view.animation.Animation.AnimationListener; 13 import android.view.animation.AnimationUtils; 14 import android.view.animation.LayoutAnimationController; 15 16 import androidx.annotation.NonNull; 17 import androidx.annotation.Nullable; 18 import androidx.recyclerview.widget.LinearLayoutManager; 19 import androidx.recyclerview.widget.RecyclerView; 20 import androidx.recyclerview.widget.RecyclerView.ItemDecoration; 21 import androidx.recyclerview.widget.RecyclerView.State; 22 23 import org.chromium.components.browser_ui.widget.R; 24 import org.chromium.ui.modelutil.ForwardingListObservable; 25 import org.chromium.ui.modelutil.PropertyModelChangeProcessor; 26 import org.chromium.ui.modelutil.RecyclerViewAdapter; 27 28 /** 29 * The View component of the tiles UI. This takes the {@link TileListModel} and creates the 30 * glue to display it on the screen. 31 */ 32 class TileListView { 33 private final TileListModel mModel; 34 private final RecyclerView mView; 35 private final RecyclerViewAdapter<TileViewHolder, Void> mAdapter; 36 private final LinearLayoutManager mLayoutManager; 37 private final LayoutAnimationController mLayoutAnimationController; 38 private final TileSizeSupplier mTileSizeSupplier; 39 40 /** Constructor. */ TileListView(Context context, TileConfig config, TileListModel model)41 public TileListView(Context context, TileConfig config, TileListModel model) { 42 mModel = model; 43 mView = new RecyclerView(context) { 44 @Override 45 protected void onConfigurationChanged(Configuration newConfig) { 46 super.onConfigurationChanged(newConfig); 47 48 // Reset the adapter to ensure that any cached views are recreated. 49 setAdapter(null); 50 setAdapter(mAdapter); 51 mTileSizeSupplier.recompute(); 52 } 53 }; 54 55 mView.setHasFixedSize(true); 56 57 mLayoutManager = new LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false); 58 mView.setLayoutManager(mLayoutManager); 59 mView.addItemDecoration(new ItemDecorationImpl(context)); 60 mView.setItemAnimator(null); 61 mLayoutAnimationController = 62 AnimationUtils.loadLayoutAnimation(context, R.anim.image_grid_enter); 63 configureAnimationListener(); 64 mTileSizeSupplier = new TileSizeSupplier(context); 65 66 PropertyModelChangeProcessor.create( 67 mModel.getProperties(), mView, new TileListPropertyViewBinder()); 68 69 mAdapter = new RecyclerViewAdapter<>( 70 new ModelChangeProcessor(mModel), new TileViewHolderFactory(mTileSizeSupplier)); 71 mView.setAdapter(mAdapter); 72 mView.post(mAdapter::notifyDataSetChanged); 73 } 74 75 /** @return The Android {@link View} representing this widget. */ getView()76 public View getView() { 77 return mView; 78 } 79 80 /** Scrolls to the beginning of the list if possible. */ scrollToBeginning()81 void scrollToBeginning() { 82 if (mView.computeHorizontalScrollOffset() != 0) { 83 mView.getLayoutManager().scrollToPosition(0); 84 } 85 } 86 87 /** 88 * Called to show enter animation for the list items. 89 */ showAnimation(boolean animate)90 void showAnimation(boolean animate) { 91 if (animate) { 92 mView.setLayoutAnimation(mLayoutAnimationController); 93 mView.scheduleLayoutAnimation(); 94 } 95 } 96 configureAnimationListener()97 private void configureAnimationListener() { 98 mView.setLayoutAnimationListener(new AnimationListener() { 99 @Override 100 public void onAnimationStart(Animation animation) {} 101 102 @Override 103 public void onAnimationEnd(Animation animation) { 104 mView.setLayoutAnimation(null); 105 } 106 107 @Override 108 public void onAnimationRepeat(Animation animation) {} 109 }); 110 } 111 112 private class ItemDecorationImpl extends ItemDecoration { 113 private final int mInterCellPadding; 114 ItemDecorationImpl(Context context)115 public ItemDecorationImpl(Context context) { 116 mInterCellPadding = context.getResources().getDimensionPixelOffset( 117 R.dimen.tile_grid_inter_tile_padding); 118 } 119 120 @Override getItemOffsets(@onNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull State state)121 public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, 122 @NonNull RecyclerView parent, @NonNull State state) { 123 int position = parent.getChildAdapterPosition(view); 124 if (position != 0) outRect.left = mInterCellPadding / 2; 125 if (position != mModel.size() - 1) outRect.right = mInterCellPadding / 2; 126 } 127 } 128 129 private static class ModelChangeProcessor extends ForwardingListObservable<Void> 130 implements RecyclerViewAdapter.Delegate<TileViewHolder, Void> { 131 private final TileListModel mModel; 132 ModelChangeProcessor(TileListModel model)133 public ModelChangeProcessor(TileListModel model) { 134 mModel = model; 135 model.addObserver(this); 136 } 137 138 @Override getItemCount()139 public int getItemCount() { 140 return mModel.size(); 141 } 142 143 @Override getItemViewType(int position)144 public int getItemViewType(int position) { 145 return 0; 146 } 147 148 @Override onBindViewHolder( TileViewHolder viewHolder, int position, @Nullable Void payload)149 public void onBindViewHolder( 150 TileViewHolder viewHolder, int position, @Nullable Void payload) { 151 viewHolder.bind(mModel.getProperties(), mModel.get(position)); 152 } 153 154 @Override onViewRecycled(TileViewHolder viewHolder)155 public void onViewRecycled(TileViewHolder viewHolder) { 156 viewHolder.recycle(); 157 } 158 } 159 } 160