blob: a3114ab7a617277a3c40df674bff006b2a92ed2b [file] [log] [blame]
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.car.ui.recyclerview;
import android.util.Log;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.StringRes;
import androidx.recyclerview.widget.RecyclerView;
/**
* A {@link RecyclerView.Adapter} that can limit its content based on a given length limit which
* can change at run-time.
*
* @param <T> type of the {@link RecyclerView.ViewHolder} objects used by base classes.
*/
public abstract class ContentLimitingAdapter<T extends RecyclerView.ViewHolder>
extends RecyclerView.Adapter<RecyclerView.ViewHolder> implements ContentLimiting {
private static final String TAG = "ContentLimitingAdapter";
private static final int SCROLLING_LIMITED_MESSAGE_VIEW_TYPE = Integer.MAX_VALUE;
private Integer mScrollingLimitedMessageResId;
private RangeFilter mRangeFilter = new PassThroughFilter();
private RecyclerView mRecyclerView;
private boolean mIsLimiting = false;
/**
* Returns the viewType value to use for the scrolling limited message views.
*
* Override this method to provide your own alternative value if {@link Integer#MAX_VALUE} is
* a viewType value already in-use by your adapter.
*/
public int getScrollingLimitedMessageViewType() {
return SCROLLING_LIMITED_MESSAGE_VIEW_TYPE;
}
@Override
@NonNull
public final RecyclerView.ViewHolder onCreateViewHolder(
@NonNull ViewGroup parent, int viewType) {
if (viewType == getScrollingLimitedMessageViewType()) {
return ScrollingLimitedViewHolder.create(parent);
}
return onCreateViewHolderImpl(parent, viewType);
}
/** See {@link RangeFilter#indexToPosition}. */
protected int indexToPosition(int index) {
return mRangeFilter.indexToPosition(index);
}
/** See {@link RangeFilter#positionToIndex}. */
protected int positionToIndex(int position) {
return mRangeFilter.positionToIndex(position);
}
/**
* Returns a {@link androidx.recyclerview.widget.RecyclerView.ViewHolder} of type {@code T}.
*
* <p>It is delegated to by {@link #onCreateViewHolder(ViewGroup, int)} to handle any
* {@code viewType}s other than the one corresponding to the "scrolling is limited" message.
*/
protected abstract T onCreateViewHolderImpl(
@NonNull ViewGroup parent, int viewType);
@Override
@SuppressWarnings("unchecked")
public final void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
if (holder instanceof ScrollingLimitedViewHolder) {
ScrollingLimitedViewHolder vh = (ScrollingLimitedViewHolder) holder;
vh.bind(mScrollingLimitedMessageResId);
} else {
int index = mRangeFilter.positionToIndex(position);
if (index != RangeFilterImpl.INVALID_INDEX) {
int size = getUnrestrictedItemCount();
if (0 <= index && index < size) {
onBindViewHolderImpl((T) holder, index);
} else {
Log.e(TAG, "onBindViewHolder pos: " + position + " gave index: "
+ index + " out of bounds size: " + size
+ " " + mRangeFilter.toString());
}
} else {
Log.e(TAG, "onBindViewHolder invalid position " + position
+ " " + mRangeFilter.toString());
}
}
}
/**
* Binds {@link androidx.recyclerview.widget.RecyclerView.ViewHolder}s of type {@code T}.
*
* <p>It is delegated to by {@link #onBindViewHolder(RecyclerView.ViewHolder, int)} to handle
* holders that are not of type {@link ScrollingLimitedViewHolder}.
*/
protected abstract void onBindViewHolderImpl(T holder, int position);
@Override
public final int getItemViewType(int position) {
if (mRangeFilter.positionToIndex(position) == RangeFilterImpl.INVALID_INDEX) {
return getScrollingLimitedMessageViewType();
} else {
return getItemViewTypeImpl(mRangeFilter.positionToIndex(position));
}
}
/**
* Returns the view type of the item at {@code position}.
*
* <p>Defaults to the implementation in {@link RecyclerView.Adapter#getItemViewType(int)}.
*
* <p>It is delegated to by {@link #getItemViewType(int)} for all positions other than the
* {@link #getScrollingLimitedMessagePosition()}.
*/
protected int getItemViewTypeImpl(int position) {
return super.getItemViewType(position);
}
/**
* Returns the position where the "scrolling is limited" message should be placed.
*
* <p>The default implementation is to put this item at the very end of the limited list.
* Subclasses can override to choose a different position to suit their needs.
*
* @deprecated limiting message offset is not supported any more.
*/
@Deprecated
protected int getScrollingLimitedMessagePosition() {
return getItemCount() - 1;
}
@Override
public final int getItemCount() {
if (mIsLimiting) {
return mRangeFilter.getFilteredCount();
} else {
return getUnrestrictedItemCount();
}
}
/**
* Returns the number of items in the unrestricted list being displayed via this adapter.
*/
protected abstract int getUnrestrictedItemCount();
@Override
@SuppressWarnings("unchecked")
public final void onViewRecycled(@NonNull RecyclerView.ViewHolder holder) {
super.onViewRecycled(holder);
if (!(holder instanceof ScrollingLimitedViewHolder)) {
onViewRecycledImpl((T) holder);
}
}
/**
* Recycles {@link androidx.recyclerview.widget.RecyclerView.ViewHolder}s of type {@code T}.
*
* <p>It is delegated to by {@link #onViewRecycled(RecyclerView.ViewHolder)} to handle
* holders that are not of type {@link ScrollingLimitedViewHolder}.
*/
@SuppressWarnings("unused")
protected void onViewRecycledImpl(@NonNull T holder) {
}
@Override
@SuppressWarnings("unchecked")
public final boolean onFailedToRecycleView(@NonNull RecyclerView.ViewHolder holder) {
if (!(holder instanceof ScrollingLimitedViewHolder)) {
return onFailedToRecycleViewImpl((T) holder);
}
return super.onFailedToRecycleView(holder);
}
/**
* Handles failed recycle attempts for
* {@link androidx.recyclerview.widget.RecyclerView.ViewHolder}s of type {@code T}.
*
* <p>It is delegated to by {@link #onFailedToRecycleView(RecyclerView.ViewHolder)} for holders
* that are not of type {@link ScrollingLimitedViewHolder}.
*/
protected boolean onFailedToRecycleViewImpl(@NonNull T holder) {
return super.onFailedToRecycleView(holder);
}
@Override
@SuppressWarnings("unchecked")
public final void onViewAttachedToWindow(@NonNull RecyclerView.ViewHolder holder) {
super.onViewAttachedToWindow(holder);
if (!(holder instanceof ScrollingLimitedViewHolder)) {
onViewAttachedToWindowImpl((T) holder);
}
}
/**
* Handles attaching {@link androidx.recyclerview.widget.RecyclerView.ViewHolder}s of type
* {@code T} to the application window.
*
* <p>It is delegated to by {@link #onViewAttachedToWindow(RecyclerView.ViewHolder)} for
* holders that are not of type {@link ScrollingLimitedViewHolder}.
*/
@SuppressWarnings("unused")
protected void onViewAttachedToWindowImpl(@NonNull T holder) {
}
@Override
public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) {
mRecyclerView = recyclerView;
}
@Override
@SuppressWarnings("unchecked")
public final void onViewDetachedFromWindow(@NonNull RecyclerView.ViewHolder holder) {
super.onViewDetachedFromWindow(holder);
if (!(holder instanceof ScrollingLimitedViewHolder)) {
onViewDetachedFromWindowImpl((T) holder);
}
}
/**
* Handles detaching {@link androidx.recyclerview.widget.RecyclerView.ViewHolder}s of type
* {@code T} from the application window.
*
* <p>It is delegated to by {@link #onViewDetachedFromWindow(RecyclerView.ViewHolder)} for
* holders that are not of type {@link ScrollingLimitedViewHolder}.
*/
@SuppressWarnings("unused")
protected void onViewDetachedFromWindowImpl(@NonNull T holder) {
}
@Override
public void setMaxItems(int maxItems) {
if (maxItems >= 0) {
if (mRangeFilter != null && mIsLimiting) {
Log.w(TAG, "A new filter range received before parked");
// remove the original filter first.
mRangeFilter.removeFilter();
}
mIsLimiting = true;
mRangeFilter = new RangeFilterImpl(this, maxItems);
mRangeFilter.recompute(getUnrestrictedItemCount(), computeAnchorIndexWhenRestricting());
mRangeFilter.applyFilter();
autoScrollWhenRestricted();
} else {
mRangeFilter.removeFilter();
mIsLimiting = false;
mRangeFilter = new PassThroughFilter();
mRangeFilter.recompute(getUnrestrictedItemCount(), 0);
}
}
/**
* Returns the position in the truncated list to scroll to when the list is limited.
*
* Returns -1 to disable the scrolling.
*/
protected int getScrollToPositionWhenRestricted() {
return -1;
}
private void autoScrollWhenRestricted() {
int scrollToPosition = getScrollToPositionWhenRestricted();
if (scrollToPosition >= 0) {
RecyclerView.LayoutManager layoutManager = mRecyclerView.getLayoutManager();
if (layoutManager != null) {
mRecyclerView.getLayoutManager().scrollToPosition(scrollToPosition);
}
}
}
/**
* Computes the anchor point index in the original list when limiting starts.
* Returns position 0 by default.
*
* Override this function to return a different anchor point to control the position of the
* limiting window.
*/
protected int computeAnchorIndexWhenRestricting() {
return 0;
}
/**
* Updates the changes from underlying data along with a new anchor.
*/
public void updateUnderlyingDataChanged(int unrestrictedCount, int newAnchorIndex) {
mRangeFilter.recompute(unrestrictedCount, newAnchorIndex);
}
/**
* Changes the index where the limiting range surrounds. Items that are added and removed will
* be notified.
*/
public void notifyLimitingAnchorChanged(int newPivotIndex) {
mRangeFilter.notifyPivotIndexChanged(newPivotIndex);
}
@Override
public void setScrollingLimitedMessageResId(@StringRes int resId) {
if (mScrollingLimitedMessageResId == null || mScrollingLimitedMessageResId != resId) {
mScrollingLimitedMessageResId = resId;
mRangeFilter.invalidateMessagePositions();
}
}
}