chromium/components/browser_ui/widget/android/java/src/org/chromium/components/browser_ui/widget/image_tiles/TileListView.java

// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.components.browser_ui.widget.image_tiles;

import android.content.Context;
import android.content.res.Configuration;
import android.graphics.Rect;
import android.view.View;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.view.animation.LayoutAnimationController;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.RecyclerView.ItemDecoration;
import androidx.recyclerview.widget.RecyclerView.State;

import org.chromium.components.browser_ui.widget.R;
import org.chromium.ui.animation.EmptyAnimationListener;
import org.chromium.ui.modelutil.ForwardingListObservable;
import org.chromium.ui.modelutil.PropertyModelChangeProcessor;
import org.chromium.ui.modelutil.RecyclerViewAdapter;

/**
 * The View component of the tiles UI.  This takes the {@link TileListModel} and creates the
 * glue to display it on the screen.
 */
class TileListView {
    private final TileListModel mModel;
    private final RecyclerView mView;
    private final RecyclerViewAdapter<TileViewHolder, Void> mAdapter;
    private final LinearLayoutManager mLayoutManager;
    private final LayoutAnimationController mLayoutAnimationController;
    private final TileSizeSupplier mTileSizeSupplier;

    /** Constructor. */
    public TileListView(Context context, TileConfig config, TileListModel model) {
        mModel = model;
        mView =
                new RecyclerView(context) {
                    @Override
                    protected void onConfigurationChanged(Configuration newConfig) {
                        super.onConfigurationChanged(newConfig);

                        // Reset the adapter to ensure that any cached views are recreated.
                        setAdapter(null);
                        setAdapter(mAdapter);
                        mTileSizeSupplier.recompute();
                    }
                };

        mView.setHasFixedSize(true);

        mLayoutManager = new LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false);
        mView.setLayoutManager(mLayoutManager);
        mView.addItemDecoration(new ItemDecorationImpl(context));
        mView.setItemAnimator(null);
        mLayoutAnimationController =
                AnimationUtils.loadLayoutAnimation(context, R.anim.image_grid_enter);
        configureAnimationListener();
        mTileSizeSupplier = new TileSizeSupplier(context);

        PropertyModelChangeProcessor.create(
                mModel.getProperties(), mView, new TileListPropertyViewBinder());

        mAdapter =
                new RecyclerViewAdapter<>(
                        new ModelChangeProcessor(mModel),
                        new TileViewHolderFactory(mTileSizeSupplier));
        mView.setAdapter(mAdapter);
        mView.post(mAdapter::notifyDataSetChanged);
    }

    /** @return The Android {@link View} representing this widget. */
    public View getView() {
        return mView;
    }

    /** Scrolls to the beginning of the list if possible. */
    void scrollToBeginning() {
        if (mView.computeHorizontalScrollOffset() != 0) {
            mView.getLayoutManager().scrollToPosition(0);
        }
    }

    /** Called to show enter animation for the list items. */
    void showAnimation(boolean animate) {
        if (animate) {
            mView.setLayoutAnimation(mLayoutAnimationController);
            mView.scheduleLayoutAnimation();
        }
    }

    private void configureAnimationListener() {
        mView.setLayoutAnimationListener(
                new EmptyAnimationListener() {
                    @Override
                    public void onAnimationEnd(Animation animation) {
                        mView.setLayoutAnimation(null);
                    }
                });
    }

    private class ItemDecorationImpl extends ItemDecoration {
        private final int mInterCellPadding;

        public ItemDecorationImpl(Context context) {
            mInterCellPadding =
                    context.getResources()
                            .getDimensionPixelOffset(R.dimen.tile_grid_inter_tile_padding);
        }

        @Override
        public void getItemOffsets(
                @NonNull Rect outRect,
                @NonNull View view,
                @NonNull RecyclerView parent,
                @NonNull State state) {
            int position = parent.getChildAdapterPosition(view);
            if (position != 0) outRect.left = mInterCellPadding / 2;
            if (position != mModel.size() - 1) outRect.right = mInterCellPadding / 2;
        }
    }

    private static class ModelChangeProcessor extends ForwardingListObservable<Void>
            implements RecyclerViewAdapter.Delegate<TileViewHolder, Void> {
        private final TileListModel mModel;

        public ModelChangeProcessor(TileListModel model) {
            mModel = model;
            model.addObserver(this);
        }

        @Override
        public int getItemCount() {
            return mModel.size();
        }

        @Override
        public int getItemViewType(int position) {
            return 0;
        }

        @Override
        public void onBindViewHolder(
                TileViewHolder viewHolder, int position, @Nullable Void payload) {
            viewHolder.bind(mModel.getProperties(), mModel.get(position));
        }

        @Override
        public void onViewRecycled(TileViewHolder viewHolder) {
            viewHolder.recycle();
        }
    }
}