blob: b25adca1bf2c9d20256db29f410e2fca503bde43 [file] [log] [blame]
// Copyright 2016 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.widget.selection;
import android.content.Context;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.drawable.Drawable;
import android.support.annotation.Nullable;
import android.support.annotation.VisibleForTesting;
import android.support.v4.view.ViewCompat;
import android.support.v4.widget.DrawerLayout;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.RecyclerView.AdapterDataObserver;
import android.support.v7.widget.RecyclerView.ItemAnimator;
import android.support.v7.widget.RecyclerView.OnScrollListener;
import android.support.v7.widget.Toolbar.OnMenuItemClickListener;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewStub;
import android.widget.FrameLayout;
import android.widget.TextView;
import org.chromium.base.ApiCompatibilityUtils;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.gesturenav.HistoryNavigationLayout;
import org.chromium.chrome.browser.widget.FadingShadow;
import org.chromium.chrome.browser.widget.FadingShadowView;
import org.chromium.chrome.browser.widget.LoadingView;
import org.chromium.chrome.browser.widget.displaystyle.DisplayStyleObserver;
import org.chromium.chrome.browser.widget.displaystyle.HorizontalDisplayStyle;
import org.chromium.chrome.browser.widget.displaystyle.UiConfig;
import org.chromium.chrome.browser.widget.displaystyle.UiConfig.DisplayStyle;
import org.chromium.chrome.browser.widget.selection.SelectionDelegate.SelectionObserver;
import java.util.List;
/**
* Contains UI elements common to selectable list views: a loading view, empty view, selection
* toolbar, shadow, and RecyclerView.
*
* After the SelectableListLayout is inflated, it should be initialized through calls to
* #initializeRecyclerView(), #initializeToolbar(), and #initializeEmptyView().
*
* @param <E> The type of the selectable items this layout holds.
*/
public class SelectableListLayout<E>
extends FrameLayout implements DisplayStyleObserver, SelectionObserver<E> {
private static final int WIDE_DISPLAY_MIN_PADDING_DP = 16;
private RecyclerView.Adapter mAdapter;
private ViewStub mToolbarStub;
private TextView mEmptyView;
private View mEmptyViewWrapper;
private LoadingView mLoadingView;
private RecyclerView mRecyclerView;
private ItemAnimator mItemAnimator;
SelectableListToolbar<E> mToolbar;
private FadingShadowView mToolbarShadow;
boolean mShowShadowOnSelection;
private int mEmptyStringResId;
private int mSearchEmptyStringResId;
private UiConfig mUiConfig;
private final AdapterDataObserver mAdapterObserver = new AdapterDataObserver() {
@Override
public void onChanged() {
super.onChanged();
updateEmptyViewVisibility();
if (mAdapter.getItemCount() == 0) {
mRecyclerView.setVisibility(View.GONE);
} else {
mRecyclerView.setVisibility(View.VISIBLE);
}
// At inflation, the RecyclerView is set to gone, and the loading view is visible. As
// long as the adapter data changes, we show the recycler view, and hide loading view.
mLoadingView.hideLoadingUI();
mToolbar.setSearchEnabled(mAdapter.getItemCount() != 0);
}
@Override
public void onItemRangeInserted(int positionStart, int itemCount) {
super.onItemRangeInserted(positionStart, itemCount);
updateEmptyViewVisibility();
}
@Override
public void onItemRangeRemoved(int positionStart, int itemCount) {
super.onItemRangeRemoved(positionStart, itemCount);
updateEmptyViewVisibility();
}
};
public SelectableListLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
LayoutInflater.from(getContext()).inflate(R.layout.selectable_list_layout, this);
mEmptyView = (TextView) findViewById(R.id.empty_view);
mEmptyViewWrapper = findViewById(R.id.empty_view_wrapper);
mLoadingView = (LoadingView) findViewById(R.id.loading_view);
mLoadingView.showLoadingUI();
mToolbarStub = (ViewStub) findViewById(R.id.action_bar_stub);
setFocusable(true);
setFocusableInTouchMode(true);
}
@Override
protected void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
if (mUiConfig != null) mUiConfig.updateDisplayStyle();
}
/**
* Creates a RecyclerView for the given adapter.
*
* @param adapter The adapter that provides a binding from an app-specific data set to views
* that are displayed within the RecyclerView.
* @return The RecyclerView itself.
*/
public RecyclerView initializeRecyclerView(RecyclerView.Adapter adapter) {
return initializeRecyclerView(adapter, null);
}
/**
* Initializes the layout with the given recycler view and adapter.
*
* @param adapter The adapter that provides a binding from an app-specific data set to views
* that are displayed within the RecyclerView.
* @param recyclerView The recycler view to be shown.
* @return The RecyclerView itself.
*/
public RecyclerView initializeRecyclerView(
RecyclerView.Adapter adapter, @Nullable RecyclerView recyclerView) {
mAdapter = adapter;
if (recyclerView == null) {
mRecyclerView = (RecyclerView) findViewById(R.id.recycler_view);
mRecyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
} else {
mRecyclerView = recyclerView;
// Replace the inflated recycler view with the one supplied to this method.
FrameLayout contentView = (FrameLayout) findViewById(R.id.list_content);
RecyclerView existingView = (RecyclerView) contentView.findViewById(R.id.recycler_view);
contentView.removeView(existingView);
contentView.addView(mRecyclerView, 0);
}
mRecyclerView.setAdapter(mAdapter);
initializeRecyclerViewProperties();
return mRecyclerView;
}
private void initializeRecyclerViewProperties() {
mAdapter.registerAdapterDataObserver(mAdapterObserver);
mRecyclerView.setHasFixedSize(true);
mRecyclerView.addOnScrollListener(new OnScrollListener() {
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
setToolbarShadowVisibility();
}
});
mItemAnimator = mRecyclerView.getItemAnimator();
}
/**
* Initializes the SelectionToolbar.
*
* @param toolbarLayoutId The resource id of the toolbar layout. This will be inflated into
* a ViewStub.
* @param delegate The SelectionDelegate that will inform the toolbar of selection changes.
* @param titleResId The resource id of the title string. May be 0 if this class shouldn't set
* set a title when the selection is cleared.
* @param drawerLayout The DrawerLayout whose navigation icon is displayed in this toolbar.
* @param normalGroupResId The resource id of the menu group to show when a selection isn't
* established.
* @param selectedGroupResId The resource id of the menu item to show when a selection is
* established.
* @param listener The OnMenuItemClickListener to set on the toolbar.
* @param showShadowOnSelection Whether to show the toolbar shadow on selection.
* @param updateStatusBarColor Whether the status bar color should be updated to match the
* toolbar color. If true, the status bar will only be updated if
* the current device fully supports theming and is on Android M+.
* @return The initialized SelectionToolbar.
*/
public SelectableListToolbar<E> initializeToolbar(int toolbarLayoutId,
SelectionDelegate<E> delegate, int titleResId, @Nullable DrawerLayout drawerLayout,
int normalGroupResId, int selectedGroupResId,
@Nullable OnMenuItemClickListener listener, boolean showShadowOnSelection,
boolean updateStatusBarColor) {
mToolbarStub.setLayoutResource(toolbarLayoutId);
@SuppressWarnings("unchecked")
SelectableListToolbar<E> toolbar = (SelectableListToolbar<E>) mToolbarStub.inflate();
mToolbar = toolbar;
mToolbar.initialize(delegate, titleResId, drawerLayout, normalGroupResId,
selectedGroupResId, updateStatusBarColor);
if (listener != null) {
mToolbar.setOnMenuItemClickListener(listener);
}
mToolbarShadow = (FadingShadowView) findViewById(R.id.shadow);
mToolbarShadow.init(
ApiCompatibilityUtils.getColor(getResources(), R.color.toolbar_shadow_color),
FadingShadow.POSITION_TOP);
mShowShadowOnSelection = showShadowOnSelection;
delegate.addObserver(this);
setToolbarShadowVisibility();
return mToolbar;
}
/**
* Initializes the view shown when the selectable list is empty.
*
* @param emptyDrawable The Drawable to show when the selectable list is empty.
* @param emptyStringResId The string to show when the selectable list is empty.
* @param searchEmptyStringResId The string to show when the selectable list is empty during
* a search.
* @return The {@link TextView} displayed when the list is empty.
*/
public TextView initializeEmptyView(
Drawable emptyDrawable, int emptyStringResId, int searchEmptyStringResId) {
mEmptyStringResId = emptyStringResId;
mSearchEmptyStringResId = searchEmptyStringResId;
mEmptyView.setCompoundDrawablesWithIntrinsicBounds(null, emptyDrawable, null, null);
mEmptyView.setText(mEmptyStringResId);
// Dummy listener to have the touch events dispatched to this view tree for navigation UI.
mEmptyViewWrapper.setOnTouchListener((v, event) -> true);
return mEmptyView;
}
/**
* Called when the view that owns the SelectableListLayout is destroyed.
*/
public void onDestroyed() {
mAdapter.unregisterAdapterDataObserver(mAdapterObserver);
mToolbar.getSelectionDelegate().removeObserver(this);
mToolbar.destroy();
mRecyclerView.setAdapter(null);
}
/**
* When this layout has a wide display style, it will be width constrained to
* {@link UiConfig#WIDE_DISPLAY_STYLE_MIN_WIDTH_DP}. If the current screen width is greater than
* UiConfig#WIDE_DISPLAY_STYLE_MIN_WIDTH_DP, the SelectableListLayout will be visually centered
* by adding padding to both sides.
*
* This method should be called after the toolbar and RecyclerView are initialized.
*/
public void configureWideDisplayStyle() {
mUiConfig = new UiConfig(this);
mToolbar.configureWideDisplayStyle(mUiConfig);
mUiConfig.addObserver(this);
}
/**
* @return The {@link UiConfig} associated with this View if one has been created, or null.
*/
@Nullable
public UiConfig getUiConfig() {
return mUiConfig;
}
@Override
public void onDisplayStyleChanged(DisplayStyle newDisplayStyle) {
int padding = getPaddingForDisplayStyle(newDisplayStyle, getResources());
ViewCompat.setPaddingRelative(mRecyclerView, padding, mRecyclerView.getPaddingTop(),
padding, mRecyclerView.getPaddingBottom());
}
@Override
public void onSelectionStateChange(List<E> selectedItems) {
setToolbarShadowVisibility();
if (!selectedItems.isEmpty()) {
((HistoryNavigationLayout) findViewById(R.id.list_content)).release();
}
}
/**
* Called when a search is starting.
*/
public void onStartSearch() {
mRecyclerView.setItemAnimator(null);
mToolbarShadow.setVisibility(View.VISIBLE);
mEmptyView.setText(mSearchEmptyStringResId);
}
/**
* Called when a search has ended.
*/
public void onEndSearch() {
mRecyclerView.setItemAnimator(mItemAnimator);
setToolbarShadowVisibility();
mEmptyView.setText(mEmptyStringResId);
}
/**
* @param displayStyle The current display style..
* @param resources The {@link Resources} used to retrieve configuration and display metrics.
* @return The lateral padding to use for the current display style.
*/
public static int getPaddingForDisplayStyle(DisplayStyle displayStyle, Resources resources) {
int padding = 0;
if (displayStyle.horizontal == HorizontalDisplayStyle.WIDE) {
int screenWidthDp = resources.getConfiguration().screenWidthDp;
float dpToPx = resources.getDisplayMetrics().density;
padding = (int) (((screenWidthDp - UiConfig.WIDE_DISPLAY_STYLE_MIN_WIDTH_DP) / 2.f)
* dpToPx);
padding = (int) Math.max(WIDE_DISPLAY_MIN_PADDING_DP * dpToPx, padding);
}
return padding;
}
private void setToolbarShadowVisibility() {
if (mToolbar == null || mRecyclerView == null) return;
boolean showShadow = mRecyclerView.canScrollVertically(-1)
|| (mToolbar.getSelectionDelegate().isSelectionEnabled() && mShowShadowOnSelection);
mToolbarShadow.setVisibility(showShadow ? View.VISIBLE : View.GONE);
}
/**
* Unlike ListView or GridView, RecyclerView does not provide default empty
* view implementation. We need to check it ourselves.
*/
private void updateEmptyViewVisibility() {
int visible = mAdapter.getItemCount() == 0 ? View.VISIBLE : View.GONE;
mEmptyView.setVisibility(visible);
mEmptyViewWrapper.setVisibility(visible);
}
@VisibleForTesting
public View getToolbarShadowForTests() {
return mToolbarShadow;
}
/**
* Called when the user presses the back key. Note that this method is not called automatically.
* The embedding UI must call this method
* when a backpress is detected for the event to be handled.
* @return Whether this event is handled.
*/
public boolean onBackPressed() {
SelectionDelegate selectionDelegate = mToolbar.getSelectionDelegate();
if (selectionDelegate.isSelectionEnabled()) {
selectionDelegate.clearSelection();
return true;
}
if (mToolbar.isSearching()) {
mToolbar.hideSearchView();
return true;
}
return false;
}
}