/**
* Copyright (c) 2017-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.facebook.litho.widget;
import android.graphics.Color;
import android.support.annotation.IdRes;
import android.support.v4.widget.SwipeRefreshLayout.OnRefreshListener;
import android.support.v7.widget.DefaultItemAnimator;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.RecyclerView.ItemAnimator;
import android.view.View;
import com.facebook.litho.ComponentContext;
import com.facebook.litho.ComponentLayout;
import com.facebook.litho.Diff;
import com.facebook.litho.EventHandler;
import com.facebook.litho.Output;
import com.facebook.litho.Size;
import com.facebook.litho.annotations.FromBind;
import com.facebook.litho.annotations.FromPrepare;
import com.facebook.litho.annotations.MountSpec;
import com.facebook.litho.annotations.OnBind;
import com.facebook.litho.annotations.OnBoundsDefined;
import com.facebook.litho.annotations.OnCreateMountContent;
import com.facebook.litho.annotations.OnMeasure;
import com.facebook.litho.annotations.OnMount;
import com.facebook.litho.annotations.OnPrepare;
import com.facebook.litho.annotations.OnUnbind;
import com.facebook.litho.annotations.OnUnmount;
import com.facebook.litho.annotations.Prop;
import com.facebook.litho.annotations.PropDefault;
import com.facebook.litho.annotations.ResType;
import com.facebook.litho.annotations.ShouldUpdate;
/**
* Components that renders a {@link RecyclerView}.
*
* @prop binder Binder for RecyclerView.
* @prop refreshHandler Event handler for refresh event.
* @prop hasFixedSize If set, makes RecyclerView not affected by adapter changes.
* @prop clipToPadding Clip RecyclerView to its padding.
* @prop nestedScrollingEnabled Enables nested scrolling on the RecyclerView.
* @prop itemDecoration Item decoration for the RecyclerView.
* @prop refreshProgressBarColor Color for progress animation.
* @prop recyclerViewId View ID for the RecyclerView.
* @prop recyclerEventsController Controller to pass events from outside the component.
* @prop onScrollListener Listener for RecyclerView's scroll events.
*/
@MountSpec(canMountIncrementally = true, isPureRender = true, events = {PTRRefreshEvent.class})
class RecyclerSpec {
@PropDefault static final int scrollBarStyle = View.SCROLLBARS_INSIDE_OVERLAY;
@PropDefault static final boolean hasFixedSize = true;
@PropDefault static final boolean nestedScrollingEnabled = true;
@PropDefault static final ItemAnimator itemAnimator = new NoUpdateItemAnimator();
@PropDefault static final int recyclerViewId = View.NO_ID;
@PropDefault static final int refreshProgressBarColor = Color.BLACK;
@OnMeasure
static void onMeasure(
ComponentContext context,
ComponentLayout layout,
int widthSpec,
int heightSpec,
Size measureOutput,
@Prop Binder<RecyclerView> binder) {
binder.measure(measureOutput, widthSpec, heightSpec);
}
@OnBoundsDefined
static void onBoundsDefined(
ComponentContext context,
ComponentLayout layout,
@Prop Binder<RecyclerView> binder) {
binder.setSize(
layout.getWidth(),
layout.getHeight());
}
@OnCreateMountContent
static RecyclerViewWrapper onCreateMountContent(ComponentContext c) {
return new RecyclerViewWrapper(c, new RecyclerView(c));
}
@OnPrepare
static void onPrepare(
ComponentContext c,
@Prop(optional = true) final EventHandler refreshHandler,
Output<OnRefreshListener> onRefreshListener) {
if (refreshHandler != null) {
onRefreshListener.set(new OnRefreshListener() {
@Override
public void onRefresh() {
Recycler.dispatchPTRRefreshEvent(refreshHandler);
}
});
}
}
@OnMount
static void onMount(
ComponentContext c,
RecyclerViewWrapper recyclerViewWrapper,
@Prop Binder<RecyclerView> binder,
@Prop(optional = true) boolean hasFixedSize,
@Prop(optional = true) boolean clipToPadding,
@Prop(optional = true) boolean nestedScrollingEnabled,
@Prop(optional = true) int scrollBarStyle,
@Prop(optional = true) RecyclerView.ItemDecoration itemDecoration,
@Prop(optional = true, resType = ResType.COLOR) int refreshProgressBarColor,
@Prop(optional = true) @IdRes int recyclerViewId) {
final RecyclerView recyclerView = recyclerViewWrapper.getRecyclerView();
if (recyclerView == null) {
throw new IllegalStateException(
"RecyclerView not found, it should not be removed from SwipeRefreshLayout");
}
recyclerViewWrapper.setColorSchemeColors(refreshProgressBarColor);
recyclerView.setHasFixedSize(hasFixedSize);
recyclerView.setClipToPadding(clipToPadding);
recyclerView.setNestedScrollingEnabled(nestedScrollingEnabled);
recyclerViewWrapper.setNestedScrollingEnabled(nestedScrollingEnabled);
recyclerView.setScrollBarStyle(scrollBarStyle);
// TODO (t14949498) determine if this is necessary
recyclerView.setId(recyclerViewId);
if (itemDecoration != null) {
recyclerView.addItemDecoration(itemDecoration);
}
binder.mount(recyclerView);
}
@OnBind
protected static void onBind(
ComponentContext context,
RecyclerViewWrapper recyclerViewWrapper,
@Prop(optional = true) ItemAnimator itemAnimator,
@Prop Binder<RecyclerView> binder,
@Prop(optional = true) final RecyclerEventsController recyclerEventsController,
@Prop(optional = true) RecyclerView.OnScrollListener onScrollListener,
@FromPrepare OnRefreshListener onRefreshListener,
Output<ItemAnimator> oldAnimator) {
recyclerViewWrapper.setEnabled(onRefreshListener != null);
recyclerViewWrapper.setOnRefreshListener(onRefreshListener);
final RecyclerView recyclerView = recyclerViewWrapper.getRecyclerView();
if (recyclerView == null) {
throw new IllegalStateException(
"RecyclerView not found, it should not be removed from SwipeRefreshLayout " +
"before unmounting");
}
oldAnimator.set(recyclerView.getItemAnimator());
if (itemAnimator != RecyclerSpec.itemAnimator) {
recyclerView.setItemAnimator(itemAnimator);
} else {
recyclerView.setItemAnimator(new NoUpdateItemAnimator());
}
if (onScrollListener != null) {
recyclerView.addOnScrollListener(onScrollListener);
}
binder.bind(recyclerView);
if (recyclerEventsController != null) {
recyclerEventsController.setRecyclerViewWrapper(recyclerViewWrapper);
}
if (recyclerViewWrapper.hasBeenDetachedFromWindow()) {
recyclerView.requestLayout();
recyclerViewWrapper.setHasBeenDetachedFromWindow(false);
}
}
@OnUnbind
static void onUnbind(
ComponentContext context,
RecyclerViewWrapper recyclerViewWrapper,
@Prop Binder<RecyclerView> binder,
@Prop(optional = true) RecyclerEventsController recyclerEventsController,
@Prop(optional = true) RecyclerView.OnScrollListener onScrollListener,
@FromBind ItemAnimator oldAnimator) {
final RecyclerView recyclerView = recyclerViewWrapper.getRecyclerView();
if (recyclerView == null) {
throw new IllegalStateException(
"RecyclerView not found, it should not be removed from SwipeRefreshLayout " +
"before unmounting");
}
recyclerView.setItemAnimator(oldAnimator);
binder.unbind(recyclerView);
if (recyclerEventsController != null) {
recyclerEventsController.setRecyclerViewWrapper(null);
}
if (onScrollListener != null) {
recyclerView.removeOnScrollListener(onScrollListener);
}
recyclerViewWrapper.setOnRefreshListener(null);
}
@OnUnmount
static void onUnmount(
ComponentContext context,
RecyclerViewWrapper recyclerViewWrapper,
@Prop Binder<RecyclerView> binder,
@Prop(optional = true) RecyclerView.ItemDecoration itemDecoration) {
final RecyclerView recyclerView = recyclerViewWrapper.getRecyclerView();
if (recyclerView == null) {
throw new IllegalStateException(
"RecyclerView not found, it should not be removed from SwipeRefreshLayout " +
"before unmounting");
}
recyclerView.setId(RecyclerSpec.recyclerViewId);
if (itemDecoration != null) {
recyclerView.removeItemDecoration(itemDecoration);
}
binder.unmount(recyclerView);
}
@ShouldUpdate(onMount = true)
protected static boolean shouldUpdate(
Diff<Binder<RecyclerView>> binder,
Diff<Boolean> hasFixedSize,
Diff<Boolean> clipToPadding,
Diff<Integer> scrollBarStyle,
Diff<RecyclerView.ItemDecoration> itemDecoration) {
if (binder.getPrevious() != binder.getNext()) {
return true;
}
if (!hasFixedSize.getPrevious().equals(hasFixedSize.getNext())) {
return true;
}
if (!clipToPadding.getPrevious().equals(clipToPadding.getNext())) {
return true;
}
if (!scrollBarStyle.getPrevious().equals(scrollBarStyle.getNext())) {
return true;
}
final RecyclerView.ItemDecoration previous = itemDecoration.getPrevious();
final RecyclerView.ItemDecoration next = itemDecoration.getNext();
final boolean itemDecorationIsEqual =
(previous == null) ? (next == null) : previous.equals(next);
return !itemDecorationIsEqual;
}
public static class NoUpdateItemAnimator extends DefaultItemAnimator {
public NoUpdateItemAnimator() {
super();
setSupportsChangeAnimations(false);
}
}
}