/** * 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.content.Context; import android.content.res.TypedArray; import android.support.v4.util.Pools.SynchronizedPool; import android.widget.HorizontalScrollView; import com.facebook.litho.R; import com.facebook.litho.ComponentContext; import com.facebook.litho.ComponentTree; import com.facebook.litho.Component; import com.facebook.litho.ComponentLayout; import com.facebook.litho.LithoView; import com.facebook.litho.Output; import com.facebook.litho.SizeSpec; import com.facebook.litho.annotations.OnCreateMountContent; import com.facebook.litho.annotations.OnLoadStyle; import com.facebook.litho.annotations.PropDefault; import com.facebook.litho.annotations.FromBoundsDefined; import com.facebook.litho.annotations.Prop; import com.facebook.litho.annotations.FromMeasure; import com.facebook.litho.annotations.FromPrepare; import com.facebook.litho.annotations.MountSpec; import com.facebook.litho.annotations.OnBoundsDefined; import com.facebook.litho.annotations.OnMeasure; import com.facebook.litho.annotations.OnMount; import com.facebook.litho.annotations.OnPrepare; import com.facebook.litho.annotations.OnUnmount; import com.facebook.litho.annotations.ResType; import com.facebook.litho.Size; import static com.facebook.litho.SizeSpec.EXACTLY; import static com.facebook.litho.SizeSpec.UNSPECIFIED; /** * A component that wraps another component and allow it to be horizontally scrollable. It's * analogous to a {@link android.widget.HorizontalScrollView}. */ @MountSpec(canMountIncrementally = true) class HorizontalScrollSpec { @PropDefault static final boolean scrollbarEnabled = true; private static final SynchronizedPool<Size> sSizePool = new SynchronizedPool<>(2); @OnLoadStyle static void onLoadStyle( ComponentContext c, Output<Boolean> scrollbarEnabled) { final TypedArray a = c.obtainStyledAttributes( R.styleable.HorizontalScroll, 0); for (int i = 0, size = a.getIndexCount(); i < size; i++) { final int attr = a.getIndex(i); if (attr == R.styleable.HorizontalScroll_android_scrollbars) { scrollbarEnabled.set(a.getInt(attr, 0) != 0); } } a.recycle(); } @OnPrepare static void onPrepare( ComponentContext context, @Prop Component<?> contentProps, Output<ComponentTree> contentComponent) { contentComponent.set( ComponentTree.create(context, contentProps).build()); } @OnMeasure static void onMeasure( ComponentContext context, ComponentLayout layout, int widthSpec, int heightSpec, Size size, @FromPrepare ComponentTree contentComponent, Output<Integer> measuredComponentWidth, Output<Integer> measuredComponentHeight) { final int measuredWidth; final int measuredHeight; Size contentSize = acquireSize(); // Measure the component with undefined width spec, as the contents of the // hscroll have unlimited horizontal space. contentComponent.setSizeSpec( SizeSpec.makeSizeSpec(0, UNSPECIFIED), heightSpec, contentSize); measuredWidth = contentSize.width; measuredHeight = contentSize.height; releaseSize(contentSize); contentSize = null; measuredComponentWidth.set(measuredWidth); measuredComponentHeight.set(measuredHeight); // If size constraints were not explicitly defined, just fallback to the // component dimensions instead. size.width = SizeSpec.getMode(widthSpec) == UNSPECIFIED ? measuredWidth : SizeSpec.getSize(widthSpec); size.height = measuredHeight; } @OnBoundsDefined static void onBoundsDefined( ComponentContext context, ComponentLayout layout, @FromPrepare ComponentTree contentComponent, @FromMeasure Integer measuredComponentWidth, @FromMeasure Integer measuredComponentHeight, Output<Integer> componentWidth, Output<Integer> componentHeight) { // If onMeasure() has been called, this means the content component already // has a defined size, no need to calculate it again. if (measuredComponentWidth != null && measuredComponentHeight != null) { componentWidth.set(measuredComponentWidth); componentHeight.set(measuredComponentHeight); } else { final int measuredWidth; final int measuredHeight; Size contentSize = acquireSize(); contentComponent.setSizeSpec( SizeSpec.makeSizeSpec(0, UNSPECIFIED), SizeSpec.makeSizeSpec(layout.getHeight(), EXACTLY), contentSize); measuredWidth = contentSize.width; measuredHeight = contentSize.height; releaseSize(contentSize); contentSize = null; componentWidth.set(measuredWidth); componentHeight.set(measuredHeight); } } @OnCreateMountContent static HorizontalScrollLithoView onCreateMountContent(ComponentContext c) { return new HorizontalScrollLithoView(c); } @OnMount static void onMount( ComponentContext context, HorizontalScrollLithoView horizontalScrollLithoView, @Prop(optional = true, resType = ResType.BOOL) boolean scrollbarEnabled, @FromPrepare ComponentTree contentComponent, @FromBoundsDefined int componentWidth, @FromBoundsDefined int componentHeight) { horizontalScrollLithoView.setHorizontalScrollBarEnabled(scrollbarEnabled); horizontalScrollLithoView.mount(contentComponent, componentWidth, componentHeight); } @OnUnmount static void onUnmount( ComponentContext context, HorizontalScrollLithoView mountedView) { mountedView.unmount(); } static class HorizontalScrollLithoView extends HorizontalScrollView { private final LithoView mLithoView; private int mComponentWidth; private int mComponentHeight; public HorizontalScrollLithoView(Context context) { super(context); mLithoView = new LithoView(context); addView(mLithoView); } @Override protected void onScrollChanged(int left, int top, int oldLeft, int oldTop) { super.onScrollChanged(left, top, oldLeft, oldTop); // Visible area changed, perform incremental mount. incrementalMount(); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { // The hosting component view always matches the component size. This will // ensure that there will never be a size-mismatch between the view and the // component-based content, which would trigger a layout pass in the // UI thread. mLithoView.measure( MeasureSpec.makeMeasureSpec(mComponentWidth, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(mComponentHeight, MeasureSpec.EXACTLY)); // The mounted view always gets exact dimensions from the framework. setMeasuredDimension( MeasureSpec.getSize(widthMeasureSpec), MeasureSpec.getSize(heightMeasureSpec)); } void mount(ComponentTree component, int width, int height) { mLithoView.setComponentTree(component); mComponentWidth = width; mComponentHeight = height; } void incrementalMount() { mLithoView.performIncrementalMount(); } void unmount() { // Clear all component-related state from the view. mLithoView.setComponentTree(null); mComponentWidth = 0; mComponentHeight = 0; } } private static Size acquireSize() { Size size = sSizePool.acquire(); if (size == null) { size = new Size(); } return size; } private static void releaseSize(Size size) { sSizePool.release(size); } }