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