blob: ad791e8303f7a7d12df47081d3b86e060a714114 [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.VisibleForTesting;
import android.support.v4.widget.DrawerLayout;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.RecyclerView.Adapter;
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.Gravity;
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.util.FeatureUtilities;
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;
import javax.annotation.Nullable;
/**
* 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 Adapter<RecyclerView.ViewHolder> mAdapter;
private ViewStub mToolbarStub;
private TextView mEmptyView;
private LoadingView mLoadingView;
private RecyclerView mRecyclerView;
private ItemAnimator mItemAnimator;
SelectableListToolbar<E> mToolbar;
private FadingShadowView mToolbarShadow;
boolean mShowShadowOnSelection;
private int mEmptyStringResId;
private int mSearchEmptyStringResId;
private int mChromeHomeEmptyAndLoadingViewTopPadding;
private UiConfig mUiConfig;
private final AdapterDataObserver mAdapterObserver = new AdapterDataObserver() {
@Override
public void onChanged() {
super.onChanged();
if (mAdapter.getItemCount() == 0) {
mEmptyView.setVisibility(View.VISIBLE);
mRecyclerView.setVisibility(View.GONE);
} else {
mEmptyView.setVisibility(View.GONE);
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.onDataChanged(mAdapter.getItemCount());
}
@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);
// TODO(twellington): Remove this fork in the code after UX decides on final design
// for empty and loading views.
mChromeHomeEmptyAndLoadingViewTopPadding =
getResources().getDimensionPixelSize(R.dimen.chrome_home_empty_view_top_padding);
mEmptyView = (TextView) findViewById(R.id.empty_view);
setEmptyOrLoadingViewStyle(mEmptyView);
mLoadingView = (LoadingView) findViewById(R.id.loading_view);
setEmptyOrLoadingViewStyle(mLoadingView);
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();
}
/**
* Initializes the RecyclerView.
*
* @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(Adapter<RecyclerView.ViewHolder> adapter) {
mAdapter = adapter;
mAdapter.registerAdapterDataObserver(mAdapterObserver);
mRecyclerView = (RecyclerView) findViewById(R.id.recycler_view);
mRecyclerView.setAdapter(mAdapter);
mRecyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
mRecyclerView.setHasFixedSize(true);
mRecyclerView.addOnScrollListener(new OnScrollListener() {
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
setToolbarShadowVisibility();
}
});
mItemAnimator = mRecyclerView.getItemAnimator();
return mRecyclerView;
}
/**
* 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 normalBackgroundColorResId The resource id of the color to use as the background color
* when selection is not enabled. If null the default appbar
* background color will be used.
* @param listener The OnMenuItemClickListener to set on the toolbar.
* @param showShadowOnSelection Whether to show the toolbar shadow on selection.
* @return The initialized SelectionToolbar.
*/
public SelectableListToolbar<E> initializeToolbar(int toolbarLayoutId,
SelectionDelegate<E> delegate, int titleResId, @Nullable DrawerLayout drawerLayout,
int normalGroupResId, int selectedGroupResId,
@Nullable Integer normalBackgroundColorResId,
@Nullable OnMenuItemClickListener listener, boolean showShadowOnSelection) {
mToolbarStub.setLayoutResource(toolbarLayoutId);
@SuppressWarnings("unchecked")
SelectableListToolbar<E> toolbar = (SelectableListToolbar<E>) mToolbarStub.inflate();
mToolbar = toolbar;
mToolbar.initialize(delegate, titleResId, drawerLayout, normalGroupResId,
selectedGroupResId, normalBackgroundColorResId);
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);
if (FeatureUtilities.isChromeModernDesignEnabled()) mToolbarShadow.setAlpha(0);
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);
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());
ApiCompatibilityUtils.setPaddingRelative(mRecyclerView,
padding, mRecyclerView.getPaddingTop(),
padding, mRecyclerView.getPaddingBottom());
}
@Override
public void onSelectionStateChange(List<E> selectedItems) {
setToolbarShadowVisibility();
}
/**
* Removes the toolbar view from this view and returns it so that it may be re-attached
* elsewhere.
* @return The toolbar view.
*/
public SelectableListToolbar<E> detachToolbarView() {
removeView(mToolbar);
return mToolbar;
}
/**
* 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.isSearching()
|| (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() {
mEmptyView.setVisibility(mAdapter.getItemCount() == 0 ? View.VISIBLE : View.GONE);
}
@VisibleForTesting
public View getToolbarShadowForTests() {
return mToolbarShadow;
}
private void setEmptyOrLoadingViewStyle(View view) {
if (!FeatureUtilities.isChromeModernDesignEnabled()) return;
((FrameLayout.LayoutParams) view.getLayoutParams()).gravity = Gravity.CENTER_HORIZONTAL;
ApiCompatibilityUtils.setPaddingRelative(view, ApiCompatibilityUtils.getPaddingStart(view),
view.getPaddingTop() + mChromeHomeEmptyAndLoadingViewTopPadding,
ApiCompatibilityUtils.getPaddingEnd(view), view.getPaddingBottom());
}
}