blob: e6af131dbc45a644eb2560b7b68941e6e25cbeb7 [file] [log] [blame]
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.chrome.browser.feed.v2;
import android.app.Activity;
import android.util.TypedValue;
import android.view.View;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import org.json.JSONException;
import org.json.JSONObject;
import org.chromium.base.Log;
import org.chromium.base.ObserverList;
import org.chromium.chrome.browser.feed.shared.stream.Header;
import org.chromium.chrome.browser.feed.shared.stream.Stream;
import org.chromium.chrome.browser.help.HelpAndFeedback;
import org.chromium.chrome.browser.native_page.NativePageNavigationDelegate;
import org.chromium.chrome.browser.ui.messages.snackbar.SnackbarManager;
import org.chromium.chrome.feed.R;
import org.chromium.components.browser_ui.bottomsheet.BottomSheetController;
import java.util.ArrayList;
import java.util.List;
/**
* A implementation of a Feed {@link Stream} that is just able to render a vertical stream of
* cards for Feed v2.
*/
public class FeedStream implements Stream {
private static final String TAG = "FeedStream";
private static final String SCROLL_POSITION = "scroll_pos";
private static final String SCROLL_OFFSET = "scroll_off";
// How far the user has to scroll down in DP before attempting to load more content.
static final int LOAD_MORE_TRIGGER_SCROLL_DISTANCE_DP = 100;
private final Activity mActivity;
private final FeedStreamSurface mFeedStreamSurface;
private final ObserverList<ScrollListener> mScrollListeners =
new ObserverList<ScrollListener>();
private final int mLoadMoreTriggerLookahead;
private RecyclerView mRecyclerView;
// setStreamContentVisibility() is always called once after onCreate(). So we can assume the
// stream content is hidden initially and it can be made visible later when
// setStreamContentVisibility() is called.
private boolean mIsStreamContentVisible = false;
// For loading more content.
private int mAccumulatedDySinceLastLoadMore;
private boolean mIsLoadingMoreContent;
public FeedStream(Activity activity, boolean isBackgroundDark, SnackbarManager snackbarManager,
NativePageNavigationDelegate nativePageNavigationDelegate,
BottomSheetController bottomSheetController) {
// TODO(petewil): Use isBackgroundDark to turn on dark theme.
this.mActivity = activity;
this.mFeedStreamSurface = new FeedStreamSurface(activity, isBackgroundDark, snackbarManager,
nativePageNavigationDelegate, bottomSheetController, HelpAndFeedback.getInstance());
this.mLoadMoreTriggerLookahead = FeedServiceBridge.getLoadMoreTriggerLookahead();
}
@Override
public void onCreate(@Nullable String savedInstanceState) {
setupRecyclerView();
if (savedInstanceState != null && !savedInstanceState.isEmpty()) {
restoreScrollState(savedInstanceState);
}
}
@Override
public void onShow() {}
@Override
public void onHide() {}
@Override
public void onDestroy() {
mFeedStreamSurface.destroy();
}
@Override
public String getSavedInstanceStateString() {
LinearLayoutManager layoutManager = (LinearLayoutManager) mRecyclerView.getLayoutManager();
if (layoutManager == null) {
return "";
}
int firstItemPosition = layoutManager.findFirstVisibleItemPosition();
if (firstItemPosition == RecyclerView.NO_POSITION) {
return "";
}
View firstVisibleView = layoutManager.findViewByPosition(firstItemPosition);
if (firstVisibleView == null) {
return "";
}
int firstVisibleTop = firstVisibleView.getTop();
JSONObject jsonSavedState = new JSONObject();
try {
jsonSavedState.put(SCROLL_POSITION, firstItemPosition);
jsonSavedState.put(SCROLL_OFFSET, firstVisibleTop);
} catch (JSONException e) {
Log.d(TAG, "Unable to write to a JSONObject.");
}
return jsonSavedState.toString();
}
@Override
public View getView() {
return mRecyclerView;
}
@Override
public void setHeaderViews(List<Header> headers) {
ArrayList<View> headerViews = new ArrayList<View>();
for (Header header : headers) {
headerViews.add(header.getView());
}
mFeedStreamSurface.setHeaderViews(headerViews);
}
@Override
public void setStreamContentVisibility(boolean visible) {
if (visible == mIsStreamContentVisible) {
return;
}
mIsStreamContentVisible = visible;
if (visible) {
mFeedStreamSurface.surfaceOpened();
} else {
mFeedStreamSurface.surfaceClosed();
}
}
@Override
public void trim() {
mRecyclerView.getRecycledViewPool().clear();
}
@Override
public void smoothScrollBy(int dx, int dy) {
mRecyclerView.smoothScrollBy(dx, dy);
}
@Override
public int getChildTopAt(int position) {
if (!isChildAtPositionVisible(position)) {
return POSITION_NOT_KNOWN;
}
LinearLayoutManager layoutManager = (LinearLayoutManager) mRecyclerView.getLayoutManager();
if (layoutManager == null) {
return POSITION_NOT_KNOWN;
}
View view = layoutManager.findViewByPosition(position);
if (view == null) {
return POSITION_NOT_KNOWN;
}
return view.getTop();
}
@Override
public boolean isChildAtPositionVisible(int position) {
LinearLayoutManager layoutManager = (LinearLayoutManager) mRecyclerView.getLayoutManager();
if (layoutManager == null) {
return false;
}
int firstItemPosition = layoutManager.findFirstVisibleItemPosition();
int lastItemPosition = layoutManager.findLastVisibleItemPosition();
if (firstItemPosition == RecyclerView.NO_POSITION
|| lastItemPosition == RecyclerView.NO_POSITION) {
return false;
}
return position >= firstItemPosition && position <= lastItemPosition;
}
@Override
public void addScrollListener(ScrollListener listener) {
mScrollListeners.addObserver(listener);
}
@Override
public void removeScrollListener(ScrollListener listener) {
mScrollListeners.removeObserver(listener);
}
@Override
public void addOnContentChangedListener(ContentChangedListener listener) {
// Not longer needed.
}
@Override
public void removeOnContentChangedListener(ContentChangedListener listener) {
// Not longer needed.
}
@Override
public void triggerRefresh() {}
private void setupRecyclerView() {
assert (!mIsStreamContentVisible);
mRecyclerView = (RecyclerView) mFeedStreamSurface.getView();
mRecyclerView.setId(R.id.feed_stream_recycler_view);
mRecyclerView.setClipToPadding(false);
mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(RecyclerView v, int dx, int dy) {
super.onScrolled(v, dx, dy);
checkScrollingForLoadMore(dy);
for (ScrollListener listener : mScrollListeners) {
listener.onScrolled(dx, dy);
}
}
});
}
@VisibleForTesting
void checkScrollingForLoadMore(int dy) {
if (!mIsStreamContentVisible) {
return;
}
mAccumulatedDySinceLastLoadMore += dy;
if (mAccumulatedDySinceLastLoadMore < TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
LOAD_MORE_TRIGGER_SCROLL_DISTANCE_DP,
mRecyclerView.getResources().getDisplayMetrics())) {
return;
}
LinearLayoutManager layoutManager = (LinearLayoutManager) mRecyclerView.getLayoutManager();
if (layoutManager == null) {
return;
}
int totalItemCount = layoutManager.getItemCount();
int lastVisibleItem = layoutManager.findLastVisibleItemPosition();
if (totalItemCount - lastVisibleItem <= mLoadMoreTriggerLookahead) {
mAccumulatedDySinceLastLoadMore = 0;
loadMore();
}
}
private void loadMore() {
if (mIsLoadingMoreContent) {
return;
}
mIsLoadingMoreContent = true;
mFeedStreamSurface.loadMoreContent((Boolean success) -> { mIsLoadingMoreContent = false; });
}
private void restoreScrollState(String savedInstanceState) {
try {
JSONObject jsonSavedState = new JSONObject(savedInstanceState);
LinearLayoutManager layoutManager =
(LinearLayoutManager) mRecyclerView.getLayoutManager();
if (layoutManager != null) {
layoutManager.scrollToPositionWithOffset(jsonSavedState.getInt(SCROLL_POSITION),
jsonSavedState.getInt(SCROLL_OFFSET));
}
} catch (JSONException e) {
Log.d(TAG, "Unable to parse a JSONObject from a string.");
}
}
}