diff --git a/Android/app/src/main/java/pub/doric/demo/MainActivity.java b/Android/app/src/main/java/pub/doric/demo/MainActivity.java index 22de4f94..b7db2405 100644 --- a/Android/app/src/main/java/pub/doric/demo/MainActivity.java +++ b/Android/app/src/main/java/pub/doric/demo/MainActivity.java @@ -50,7 +50,7 @@ public class MainActivity extends AppCompatActivity { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); - doricContext = DoricContext.create(this, DoricUtils.readAssetFile("demo/ListDemo.js"), "test"); + doricContext = DoricContext.create(this, DoricUtils.readAssetFile("demo/ScrollerDemo.js"), "test"); doricContext.init(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); doricContext.getRootNode().setRootView((FrameLayout) findViewById(R.id.root)); diff --git a/Android/doric/src/main/java/pub/doric/DoricRegistry.java b/Android/doric/src/main/java/pub/doric/DoricRegistry.java index edc337b6..3747cdce 100644 --- a/Android/doric/src/main/java/pub/doric/DoricRegistry.java +++ b/Android/doric/src/main/java/pub/doric/DoricRegistry.java @@ -20,6 +20,7 @@ import android.text.TextUtils; import pub.doric.plugin.ShaderPlugin; import pub.doric.shader.HLayoutNode; import pub.doric.shader.ImageNode; +import pub.doric.shader.ScrollerNode; import pub.doric.shader.list.ListItemNode; import pub.doric.shader.list.ListNode; import pub.doric.shader.RootNode; @@ -70,6 +71,7 @@ public class DoricRegistry { this.registerViewNode(HLayoutNode.class); this.registerViewNode(ListNode.class); this.registerViewNode(ListItemNode.class); + this.registerViewNode(ScrollerNode.class); initRegistry(this); } diff --git a/Android/doric/src/main/java/pub/doric/shader/GroupNode.java b/Android/doric/src/main/java/pub/doric/shader/GroupNode.java index 0220a686..e1316229 100644 --- a/Android/doric/src/main/java/pub/doric/shader/GroupNode.java +++ b/Android/doric/src/main/java/pub/doric/shader/GroupNode.java @@ -34,8 +34,6 @@ public abstract class GroupNode extends SuperNode { private ArrayList mChildNodes = new ArrayList<>(); private ArrayList mChildViewIds = new ArrayList<>(); - protected boolean mReusable = false; - public GroupNode(DoricContext doricContext) { super(doricContext); } @@ -80,8 +78,8 @@ public abstract class GroupNode extends SuperNode { mView.removeView(oldNode.getDoricLayer()); ViewNode newNode = ViewNode.create(getDoricContext(), type); newNode.setId(id); - if (newNode instanceof GroupNode) { - ((GroupNode) newNode).mReusable = this.mReusable; + if (newNode instanceof SuperNode) { + ((SuperNode) newNode).mReusable = this.mReusable; } newNode.init(this); newNode.blend(model.getProperty("props").asObject()); @@ -126,8 +124,8 @@ public abstract class GroupNode extends SuperNode { //Insert ViewNode newNode = ViewNode.create(getDoricContext(), type); newNode.setId(id); - if (newNode instanceof GroupNode) { - ((GroupNode) newNode).mReusable = this.mReusable; + if (newNode instanceof SuperNode) { + ((SuperNode) newNode).mReusable = this.mReusable; } newNode.init(this); newNode.blend(model.getProperty("props").asObject()); diff --git a/Android/doric/src/main/java/pub/doric/shader/ScrollerNode.java b/Android/doric/src/main/java/pub/doric/shader/ScrollerNode.java new file mode 100644 index 00000000..028a4eea --- /dev/null +++ b/Android/doric/src/main/java/pub/doric/shader/ScrollerNode.java @@ -0,0 +1,101 @@ +/* + * Copyright [2019] [Doric.Pub] + * + * 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 pub.doric.shader; + + +import com.github.pengfeizhou.jscore.JSObject; +import com.github.pengfeizhou.jscore.JSValue; + +import pub.doric.DoricContext; +import pub.doric.extension.bridge.DoricPlugin; +import pub.doric.widget.HVScrollView; + +/** + * @Description: pub.doric.shader + * @Author: pengfei.zhou + * @CreateDate: 2019-11-18 + */ +@DoricPlugin(name = "Scroller") +public class ScrollerNode extends SuperNode { + private String mChildViewId; + private ViewNode mChildNode; + + public ScrollerNode(DoricContext doricContext) { + super(doricContext); + } + + @Override + public ViewNode getSubNodeById(String id) { + return id.equals(mChildNode.getId()) ? mChildNode : null; + } + + @Override + protected void blendSubNode(JSObject subProperties) { + if (mChildNode != null) { + mChildNode.blend(subProperties); + } + } + + @Override + protected HVScrollView build() { + return new HVScrollView(getContext()); + } + + @Override + protected void blend(HVScrollView view, String name, JSValue prop) { + if ("content".equals(name)) { + mChildViewId = prop.asString().value(); + } else { + super.blend(view, name, prop); + } + } + + @Override + public void blend(JSObject jsObject) { + super.blend(jsObject); + JSObject contentModel = getSubModel(mChildViewId); + if (contentModel == null) { + return; + } + String viewId = contentModel.getProperty("id").asString().value(); + String type = contentModel.getProperty("type").asString().value(); + JSObject props = contentModel.getProperty("props").asObject(); + if (mChildNode != null) { + if (mChildNode.getId().equals(viewId)) { + //skip + } else { + if (mReusable && type.equals(mChildNode.getType())) { + mChildNode.setId(viewId); + mChildNode.blend(props); + } else { + mView.removeAllViews(); + mChildNode = ViewNode.create(getDoricContext(), type); + mChildNode.setId(viewId); + mChildNode.init(this); + mChildNode.blend(props); + mView.addView(mChildNode.getDoricLayer()); + } + } + } else { + mChildNode = ViewNode.create(getDoricContext(), type); + mChildNode.setId(viewId); + mChildNode.init(this); + mChildNode.blend(props); + mView.addView(mChildNode.getDoricLayer()); + } + } + +} diff --git a/Android/doric/src/main/java/pub/doric/shader/SuperNode.java b/Android/doric/src/main/java/pub/doric/shader/SuperNode.java index 954b0689..6cdcadae 100644 --- a/Android/doric/src/main/java/pub/doric/shader/SuperNode.java +++ b/Android/doric/src/main/java/pub/doric/shader/SuperNode.java @@ -35,6 +35,7 @@ import pub.doric.utils.DoricUtils; */ public abstract class SuperNode extends ViewNode { private Map subNodes = new HashMap<>(); + protected boolean mReusable = false; public SuperNode(DoricContext doricContext) { super(doricContext); diff --git a/Android/doric/src/main/java/pub/doric/shader/TextNode.java b/Android/doric/src/main/java/pub/doric/shader/TextNode.java index b83b8d27..246a16f9 100644 --- a/Android/doric/src/main/java/pub/doric/shader/TextNode.java +++ b/Android/doric/src/main/java/pub/doric/shader/TextNode.java @@ -37,7 +37,9 @@ public class TextNode extends ViewNode { @Override protected TextView build() { - return new TextView(getContext()); + TextView tv = new TextView(getContext()); + tv.setGravity(Gravity.CENTER); + return tv; } @Override diff --git a/Android/doric/src/main/java/pub/doric/widget/HVScrollView.java b/Android/doric/src/main/java/pub/doric/widget/HVScrollView.java new file mode 100644 index 00000000..f74db4e8 --- /dev/null +++ b/Android/doric/src/main/java/pub/doric/widget/HVScrollView.java @@ -0,0 +1,2284 @@ +/* + * Copyright [2019] [Doric.Pub] + * + * 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 pub.doric.widget; + + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.AttributeSet; +import android.util.Log; +import android.util.TypedValue; +import android.view.FocusFinder; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.VelocityTracker; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.ViewGroup; +import android.view.ViewParent; +import android.view.accessibility.AccessibilityEvent; +import android.view.animation.AnimationUtils; +import android.widget.EdgeEffect; +import android.widget.FrameLayout; +import android.widget.OverScroller; +import android.widget.ScrollView; + +import androidx.annotation.RestrictTo; +import androidx.core.view.AccessibilityDelegateCompat; +import androidx.core.view.InputDeviceCompat; +import androidx.core.view.NestedScrollingChild2; +import androidx.core.view.NestedScrollingChildHelper; +import androidx.core.view.NestedScrollingParent; +import androidx.core.view.NestedScrollingParentHelper; +import androidx.core.view.ScrollingView; +import androidx.core.view.ViewCompat; +import androidx.core.view.accessibility.AccessibilityNodeInfoCompat; +import androidx.core.view.accessibility.AccessibilityRecordCompat; +import androidx.core.widget.EdgeEffectCompat; + +import java.util.List; + +/** + * NestedScrollView is just like {@link android.widget.ScrollView}, but it supports acting + * as both a nested scrolling parent and child on both new and old versions of Android. + * Nested scrolling is enabled by default. + */ +public class HVScrollView extends FrameLayout implements NestedScrollingParent, + NestedScrollingChild2, ScrollingView { + static final int ANIMATED_SCROLL_GAP = 250; + + static final float MAX_SCROLL_FACTOR = 0.5f; + + private static final String TAG = "NestedScrollView"; + + /** + * Interface definition for a callback to be invoked when the scroll + * X or Y positions of a view change. + *

+ *

This version of the interface works on all versions of Android, back to API v4.

+ * + * @see #setOnScrollChangeListener(OnScrollChangeListener) + */ + public interface OnScrollChangeListener { + /** + * Called when the scroll position of a view changes. + * + * @param v The view whose scroll position has changed. + * @param scrollX Current horizontal scroll origin. + * @param scrollY Current vertical scroll origin. + * @param oldScrollX Previous horizontal scroll origin. + * @param oldScrollY Previous vertical scroll origin. + */ + void onScrollChange(HVScrollView v, int scrollX, int scrollY, + int oldScrollX, int oldScrollY); + } + + private long mLastScroll; + + private final Rect mTempRect = new Rect(); + private OverScroller mScroller; + private EdgeEffect mEdgeGlowTop; + private EdgeEffect mEdgeGlowBottom; + private EdgeEffect mEdgeGlowLeft; + private EdgeEffect mEdgeGlowRight; + private int scrollModeFlag = 0; + public static final int DISABLE_VERTICAL_SCROLL = 1; + public static final int DISABLE_HORIZONTAL_SCROLL = 2; + + /** + * Position of the last motion event. + */ + private int mLastMotionX; + private int mLastMotionY; + + /** + * True when the layout has changed but the traversal has not come through yet. + * Ideally the view hierarchy would keep track of this for us. + */ + private boolean mIsLayoutDirty = true; + private boolean mIsLaidOut = false; + + /** + * The child to give focus to in the event that a child has requested focus while the + * layout is dirty. This prevents the scroll from being wrong if the child has not been + * laid out before requesting focus. + */ + private View mChildToScrollTo = null; + + /** + * True if the user is currently dragging this ScrollView around. This is + * not the same as 'is being flinged', which can be checked by + * mScroller.isFinished() (flinging begins when the user lifts his finger). + */ + private boolean mIsBeingDragged = false; + + /** + * Determines speed during touch scrolling + */ + private VelocityTracker mVelocityTracker; + + /** + * When set to true, the scroll view measure its child to make it fill the currently + * visible area. + */ + private boolean mFillViewport; + + /** + * Whether arrow scrolling is animated. + */ + private boolean mSmoothScrollingEnabled = true; + + private int mTouchSlop; + private int mMinimumVelocity; + private int mMaximumVelocity; + + /** + * ID of the active pointer. This is used to retain consistency during + * drags/flings if multiple pointers are used. + */ + private int mActivePointerId = INVALID_POINTER; + + /** + * Used during scrolling to retrieve the new offset within the window. + */ + private final int[] mScrollOffset = new int[2]; + private final int[] mScrollConsumed = new int[2]; + private int mNestedXOffset; + private int mNestedYOffset; + + private int mLastScrollerX; + private int mLastScrollerY; + + /** + * Sentinel value for no current active pointer. + * Used by {@link #mActivePointerId}. + */ + private static final int INVALID_POINTER = -1; + + private SavedState mSavedState; + + private static final AccessibilityDelegate ACCESSIBILITY_DELEGATE = new AccessibilityDelegate(); + + private static final int[] SCROLLVIEW_STYLEABLE = new int[]{ + android.R.attr.fillViewport + }; + + private final NestedScrollingParentHelper mParentHelper; + private final NestedScrollingChildHelper mChildHelper; + + private float mVerticalScrollFactor; + private float mHorizontalScrollFactor; + + private OnScrollChangeListener mOnScrollChangeListener; + + public HVScrollView(Context context) { + this(context, null); + } + + public HVScrollView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public HVScrollView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + initScrollView(); + + final TypedArray a = context.obtainStyledAttributes( + attrs, SCROLLVIEW_STYLEABLE, defStyleAttr, 0); + + setFillViewport(a.getBoolean(0, false)); + + a.recycle(); + + mParentHelper = new NestedScrollingParentHelper(this); + mChildHelper = new NestedScrollingChildHelper(this); + + // ...because why else would you be using this widget? + setNestedScrollingEnabled(true); + + ViewCompat.setAccessibilityDelegate(this, ACCESSIBILITY_DELEGATE); + } + + // NestedScrollingChild + + @Override + public void setNestedScrollingEnabled(boolean enabled) { + mChildHelper.setNestedScrollingEnabled(enabled); + } + + @Override + public boolean isNestedScrollingEnabled() { + return mChildHelper.isNestedScrollingEnabled(); + } + + @Override + public boolean startNestedScroll(int axes) { + return mChildHelper.startNestedScroll(axes); + } + + @Override + public boolean startNestedScroll(int axes, int type) { + return mChildHelper.startNestedScroll(axes, type); + } + + @Override + public void stopNestedScroll() { + mChildHelper.stopNestedScroll(); + } + + @Override + public void stopNestedScroll(int type) { + mChildHelper.stopNestedScroll(type); + } + + @Override + public boolean hasNestedScrollingParent() { + return mChildHelper.hasNestedScrollingParent(); + } + + @Override + public boolean hasNestedScrollingParent(int type) { + return mChildHelper.hasNestedScrollingParent(type); + } + + @Override + public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, + int dyUnconsumed, int[] offsetInWindow) { + return mChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, + offsetInWindow); + } + + @Override + public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, + int dyUnconsumed, int[] offsetInWindow, int type) { + return mChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, + offsetInWindow, type); + } + + @Override + public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) { + return mChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow); + } + + @Override + public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow, + int type) { + return mChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow, type); + } + + @Override + public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) { + return mChildHelper.dispatchNestedFling(velocityX, velocityY, consumed); + } + + @Override + public boolean dispatchNestedPreFling(float velocityX, float velocityY) { + return mChildHelper.dispatchNestedPreFling(velocityX, velocityY); + } + + // NestedScrollingParent + + @Override + public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) { + return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0 + || (nestedScrollAxes & ViewCompat.SCROLL_AXIS_HORIZONTAL) != 0; + } + + @Override + public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes) { + mParentHelper.onNestedScrollAccepted(child, target, nestedScrollAxes); + startNestedScroll(nestedScrollAxes); + } + + @Override + public void onStopNestedScroll(View target) { + mParentHelper.onStopNestedScroll(target); + stopNestedScroll(); + } + + @Override + public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, + int dyUnconsumed) { + final int oldScrollX = getScrollX(); + final int oldScrollY = getScrollY(); + scrollBy(dxUnconsumed, dyUnconsumed); + final int myConsumedX = getScrollX() - oldScrollX; + final int myUnconsumedX = dxUnconsumed - myConsumedX; + final int myConsumedY = getScrollY() - oldScrollY; + final int myUnconsumedY = dyUnconsumed - myConsumedY; + dispatchNestedScroll(myConsumedX, myConsumedY, myUnconsumedX, myUnconsumedY, null); + } + + @Override + public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) { + dispatchNestedPreScroll(dx, dy, consumed, null); + } + + @Override + public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) { + if (!consumed) { + flingWithNestedDispatch((int) velocityX, (int) velocityY); + return true; + } + return false; + } + + @Override + public boolean onNestedPreFling(View target, float velocityX, float velocityY) { + return dispatchNestedPreFling(velocityX, velocityY); + } + + @Override + public int getNestedScrollAxes() { + return mParentHelper.getNestedScrollAxes(); + } + + // ScrollView import + + @Override + public boolean shouldDelayChildPressedState() { + return true; + } + + @Override + protected float getTopFadingEdgeStrength() { + if (getChildCount() == 0) { + return 0.0f; + } + + final int length = getVerticalFadingEdgeLength(); + final int scrollY = getScrollY(); + if (scrollY < length) { + return scrollY / (float) length; + } + + return 1.0f; + } + + @Override + protected float getBottomFadingEdgeStrength() { + if (getChildCount() == 0) { + return 0.0f; + } + + final int length = getVerticalFadingEdgeLength(); + final int bottomEdge = getHeight() - getPaddingBottom(); + final int span = getChildAt(0).getBottom() - getScrollY() - bottomEdge; + if (span < length) { + return span / (float) length; + } + + return 1.0f; + } + + @Override + protected float getLeftFadingEdgeStrength() { + if (getChildCount() == 0) { + return 0.0f; + } + + final int length = getHorizontalFadingEdgeLength(); + final int scrollX = getScrollX(); + if (scrollX < length) { + return scrollX / (float) length; + } + + return 1.0f; + } + + @Override + protected float getRightFadingEdgeStrength() { + if (getChildCount() == 0) { + return 0.0f; + } + + final int length = getHorizontalFadingEdgeLength(); + final int rightEdge = getWidth() - getPaddingRight(); + final int span = getChildAt(0).getRight() - getScrollY() - rightEdge; + if (span < length) { + return span / (float) length; + } + + return 1.0f; + } + + /** + * @return The maximum amount this scroll view will scroll in response to + * an arrow event. + */ + public int getMaxScrollAmount() { + return (int) (MAX_SCROLL_FACTOR * getHeight()); + } + + private void initScrollView() { + mScroller = new OverScroller(getContext()); + setFocusable(true); + setDescendantFocusability(FOCUS_AFTER_DESCENDANTS); + setWillNotDraw(false); + final ViewConfiguration configuration = ViewConfiguration.get(getContext()); + mTouchSlop = configuration.getScaledTouchSlop(); + mMinimumVelocity = configuration.getScaledMinimumFlingVelocity(); + mMaximumVelocity = configuration.getScaledMaximumFlingVelocity(); + } + + @Override + public void addView(View child) { + if (getChildCount() > 0) { + throw new IllegalStateException("ScrollView can host only one direct child"); + } + + super.addView(child); + } + + @Override + public void addView(View child, int index) { + if (getChildCount() > 0) { + throw new IllegalStateException("ScrollView can host only one direct child"); + } + + super.addView(child, index); + } + + @Override + public void addView(View child, ViewGroup.LayoutParams params) { + if (getChildCount() > 0) { + throw new IllegalStateException("ScrollView can host only one direct child"); + } + + super.addView(child, params); + } + + @Override + public void addView(View child, int index, ViewGroup.LayoutParams params) { + if (getChildCount() > 0) { + throw new IllegalStateException("ScrollView can host only one direct child"); + } + + super.addView(child, index, params); + } + + /** + * Register a callback to be invoked when the scroll X or Y positions of + * this view change. + *

This version of the method works on all versions of Android, back to API v4.

+ * + * @param l The listener to notify when the scroll X or Y position changes. + * @see android.view.View#getScrollX() + * @see android.view.View#getScrollY() + */ + public void setOnScrollChangeListener(OnScrollChangeListener l) { + mOnScrollChangeListener = l; + } + + /** + * @return Returns true this ScrollView can be scrolled + */ + private boolean canScroll() { + return canScrollHorizontally() || canScrollVertically(); + } + + private boolean canScrollVertically() { + View child = getChildAt(0); + if (child != null) { + int childHeight = child.getHeight(); + return getHeight() < childHeight + getPaddingTop() + getPaddingBottom(); + } + return false; + } + + private boolean canScrollHorizontally() { + View child = getChildAt(0); + if (child != null) { + int childWidth = child.getWidth(); + return getWidth() < childWidth + getPaddingLeft() + getPaddingRight(); + } + return false; + } + + /** + * Indicates whether this ScrollView's content is stretched to fill the viewport. + * + * @return True if the content fills the viewport, false otherwise. + * @attr name android:fillViewport + */ + public boolean isFillViewport() { + return mFillViewport; + } + + /** + * Set whether this ScrollView should stretch its content height to fill the viewport or not. + * + * @param fillViewport True to stretch the content's height to the viewport's + * boundaries, false otherwise. + * @attr name android:fillViewport + */ + public void setFillViewport(boolean fillViewport) { + if (fillViewport != mFillViewport) { + mFillViewport = fillViewport; + requestLayout(); + } + } + + /** + * @return Whether arrow scrolling will animate its transition. + */ + public boolean isSmoothScrollingEnabled() { + return mSmoothScrollingEnabled; + } + + /** + * Set whether arrow scrolling will animate its transition. + * + * @param smoothScrollingEnabled whether arrow scrolling will animate its transition + */ + public void setSmoothScrollingEnabled(boolean smoothScrollingEnabled) { + mSmoothScrollingEnabled = smoothScrollingEnabled; + } + + @Override + protected void onScrollChanged(int l, int t, int oldl, int oldt) { + super.onScrollChanged(l, t, oldl, oldt); + + if (mOnScrollChangeListener != null) { + mOnScrollChangeListener.onScrollChange(this, l, t, oldl, oldt); + } + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + + if (!mFillViewport) { + return; + } + + final int widthMode = MeasureSpec.getMode(widthMeasureSpec); + final int heightMode = MeasureSpec.getMode(heightMeasureSpec); + if (heightMode == MeasureSpec.UNSPECIFIED && widthMode == MeasureSpec.UNSPECIFIED) { + return; + } + + if (getChildCount() > 0) { + final View child = getChildAt(0); + int width = getMeasuredWidth(); + int height = getMeasuredHeight(); + width -= getPaddingLeft(); + width -= getPaddingRight(); + height -= getPaddingTop(); + height -= getPaddingBottom(); + int childWidthMeasureSpec; + int childHeightMeasureSpec; + final FrameLayout.LayoutParams lp = (LayoutParams) child.getLayoutParams(); + if (child.getMeasuredWidth() < width) { + childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY); + } else { + widthMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); + childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, + getPaddingLeft() + getPaddingRight(), lp.width); + } + if (child.getMeasuredHeight() < height) { + childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY); + } else { + heightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); + childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec, + getPaddingTop() + getPaddingBottom(), lp.height); + } + child.measure(childWidthMeasureSpec, childHeightMeasureSpec); + } + } + + @Override + public boolean dispatchKeyEvent(KeyEvent event) { + // Let the focused view and/or our descendants get the key first + return super.dispatchKeyEvent(event) || executeKeyEvent(event); + } + + /** + * You can call this function yourself to have the scroll view perform + * scrolling from a key event, just as if the event had been dispatched to + * it by the view hierarchy. + * + * @param event The key event to execute. + * @return Return true if the event was handled, else false. + */ + public boolean executeKeyEvent(KeyEvent event) { + mTempRect.setEmpty(); + + if (!canScroll()) { + if (isFocused() && event.getKeyCode() != KeyEvent.KEYCODE_BACK) { + View currentFocused = findFocus(); + if (currentFocused == this) currentFocused = null; + View nextFocused = FocusFinder.getInstance().findNextFocus(this, + currentFocused, View.FOCUS_DOWN); + return nextFocused != null + && nextFocused != this + && nextFocused.requestFocus(View.FOCUS_DOWN); + } + return false; + } + + boolean handled = false; + if (event.getAction() == KeyEvent.ACTION_DOWN) { + switch (event.getKeyCode()) { + case KeyEvent.KEYCODE_DPAD_UP: + if (!event.isAltPressed()) { + handled = arrowScroll(View.FOCUS_UP); + } else { + handled = fullScroll(View.FOCUS_UP); + } + break; + case KeyEvent.KEYCODE_DPAD_DOWN: + if (!event.isAltPressed()) { + handled = arrowScroll(View.FOCUS_DOWN); + } else { + handled = fullScroll(View.FOCUS_DOWN); + } + break; + case KeyEvent.KEYCODE_SPACE: + pageScroll(event.isShiftPressed() ? View.FOCUS_UP : View.FOCUS_DOWN); + break; + } + } + + return handled; + } + + private boolean inChild(int x, int y) { + if (getChildCount() > 0) { + final int scrollY = getScrollY(); + final int scrollX = getScrollX(); + final View child = getChildAt(0); + return !(y < child.getTop() - scrollY + || y >= child.getBottom() - scrollY + || x < child.getLeft() - scrollX + || x >= child.getRight() - scrollX); + } + return false; + } + + private void initOrResetVelocityTracker() { + if (mVelocityTracker == null) { + mVelocityTracker = VelocityTracker.obtain(); + } else { + mVelocityTracker.clear(); + } + } + + private void initVelocityTrackerIfNotExists() { + if (mVelocityTracker == null) { + mVelocityTracker = VelocityTracker.obtain(); + } + } + + private void recycleVelocityTracker() { + if (mVelocityTracker != null) { + mVelocityTracker.recycle(); + mVelocityTracker = null; + } + } + + @Override + public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) { + if (disallowIntercept) { + recycleVelocityTracker(); + } + super.requestDisallowInterceptTouchEvent(disallowIntercept); + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + /* + * This method JUST determines whether we want to intercept the motion. + * If we return true, onMotionEvent will be called and we do the actual + * scrolling there. + */ + + /* + * Shortcut the most recurring case: the user is in the dragging + * state and he is moving his finger. We want to intercept this + * motion. + */ + final int action = ev.getAction(); + if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) { + return true; + } + + switch (action & MotionEvent.ACTION_MASK) { + case MotionEvent.ACTION_MOVE: { + /* + * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check + * whether the user has moved far enough from his original down touch. + */ + + /* + * Locally do absolute value. mLastMotionY is set to the y value + * of the down event. + */ + final int activePointerId = mActivePointerId; + if (activePointerId == INVALID_POINTER) { + // If we don't have a valid id, the touch down wasn't on content. + break; + } + + final int pointerIndex = ev.findPointerIndex(activePointerId); + if (pointerIndex == -1) { + Log.e(TAG, "Invalid pointerId=" + activePointerId + + " in onInterceptTouchEvent"); + break; + } + + final int x = (int) ev.getX(pointerIndex); + final int y = (int) ev.getY(pointerIndex); + final int xDiff = Math.abs(x - mLastMotionX); + final int yDiff = Math.abs(y - mLastMotionY); + if ((xDiff > mTouchSlop && (getNestedScrollAxes() & ViewCompat.SCROLL_AXIS_HORIZONTAL) == 0) + || (yDiff > mTouchSlop && (getNestedScrollAxes() & ViewCompat.SCROLL_AXIS_VERTICAL) == 0)) { + mIsBeingDragged = true; + mLastMotionX = x; + mLastMotionY = y; + initVelocityTrackerIfNotExists(); + mVelocityTracker.addMovement(ev); + mNestedXOffset = 0; + mNestedYOffset = 0; + final ViewParent parent = getParent(); + if (parent != null) { + parent.requestDisallowInterceptTouchEvent(true); + } + } + break; + } + + case MotionEvent.ACTION_DOWN: { + final int x = (int) ev.getX(); + final int y = (int) ev.getY(); + if (!inChild(x, y)) { + mIsBeingDragged = false; + recycleVelocityTracker(); + break; + } + + /* + * Remember location of down touch. + * ACTION_DOWN always refers to pointer index 0. + */ + mLastMotionX = x; + mLastMotionY = y; + mActivePointerId = ev.getPointerId(0); + + initOrResetVelocityTracker(); + mVelocityTracker.addMovement(ev); + /* + * If being flinged and user touches the screen, initiate drag; + * otherwise don't. mScroller.isFinished should be false when + * being flinged. We need to call computeScrollOffset() first so that + * isFinished() is correct. + */ + mScroller.computeScrollOffset(); + mIsBeingDragged = !mScroller.isFinished(); + int axes = getAxes(); + startNestedScroll(axes, ViewCompat.TYPE_TOUCH); + break; + } + + case MotionEvent.ACTION_CANCEL: + case MotionEvent.ACTION_UP: + /* Release the drag */ + mIsBeingDragged = false; + mActivePointerId = INVALID_POINTER; + recycleVelocityTracker(); + if (mScroller.springBack(getScrollX(), getScrollY(), 0, getScrollRangeX(), 0, getScrollRangeY())) { + ViewCompat.postInvalidateOnAnimation(this); + } + stopNestedScroll(ViewCompat.TYPE_TOUCH); + break; + case MotionEvent.ACTION_POINTER_UP: + onSecondaryPointerUp(ev); + break; + } + + /* + * The only time we want to intercept motion events is if we are in the + * drag mode. + */ + return mIsBeingDragged; + } + + @Override + public boolean onTouchEvent(MotionEvent ev) { + initVelocityTrackerIfNotExists(); + + MotionEvent vtev = MotionEvent.obtain(ev); + + final int actionMasked = ev.getActionMasked(); + + if (actionMasked == MotionEvent.ACTION_DOWN) { + mNestedXOffset = 0; + mNestedYOffset = 0; + } + vtev.offsetLocation(mNestedXOffset, mNestedYOffset); + + switch (actionMasked) { + case MotionEvent.ACTION_DOWN: { + if (getChildCount() == 0) { + return false; + } + if ((mIsBeingDragged = !mScroller.isFinished())) { + final ViewParent parent = getParent(); + if (parent != null) { + parent.requestDisallowInterceptTouchEvent(true); + } + } + + /* + * If being flinged and user touches, stop the fling. isFinished + * will be false if being flinged. + */ + if (!mScroller.isFinished()) { + mScroller.abortAnimation(); + } + + // Remember where the motion event started + mLastMotionX = (int) ev.getX(); + mLastMotionY = (int) ev.getY(); + mActivePointerId = ev.getPointerId(0); + int axes = getAxes(); + startNestedScroll(axes, ViewCompat.TYPE_TOUCH); + break; + } + case MotionEvent.ACTION_MOVE: + final int activePointerIndex = ev.findPointerIndex(mActivePointerId); + if (activePointerIndex == -1) { + Log.e(TAG, "Invalid pointerId=" + mActivePointerId + " in onTouchEvent"); + break; + } + + final int x = (int) ev.getX(activePointerIndex); + final int y = (int) ev.getY(activePointerIndex); + int deltaX = mLastMotionX - x; + int deltaY = mLastMotionY - y; + if (dispatchNestedPreScroll(deltaX, deltaY, mScrollConsumed, mScrollOffset, + ViewCompat.TYPE_TOUCH)) { + deltaX -= mScrollConsumed[0]; + deltaY -= mScrollConsumed[1]; + vtev.offsetLocation(mScrollOffset[0], mScrollOffset[1]); + mNestedXOffset += mScrollOffset[0]; + mNestedYOffset += mScrollOffset[1]; + } + if (!mIsBeingDragged && (Math.abs(deltaX) > mTouchSlop || Math.abs(deltaY) > mTouchSlop)) { + final ViewParent parent = getParent(); + if (parent != null) { + parent.requestDisallowInterceptTouchEvent(true); + } + mIsBeingDragged = true; + if (deltaX > 0) { + deltaX -= mTouchSlop; + } else { + deltaX += mTouchSlop; + } + if (deltaY > 0) { + deltaY -= mTouchSlop; + } else { + deltaY += mTouchSlop; + } + } + if (mIsBeingDragged) { + // Scroll to follow the motion event + mLastMotionX = x - mScrollOffset[0]; + mLastMotionY = y - mScrollOffset[1]; + + final int oldX = getScrollX(); + final int oldY = getScrollY(); + final int rangeX = getScrollRangeX(); + final int rangeY = getScrollRangeY(); + final int overscrollMode = getOverScrollMode(); + boolean canOverscroll = overscrollMode == View.OVER_SCROLL_ALWAYS + || (overscrollMode == View.OVER_SCROLL_IF_CONTENT_SCROLLS && (rangeX > 0 || rangeY > 0)); + + // Calling overScrollByCompat will call onOverScrolled, which + // calls onScrollChanged if applicable. + if (overScrollByCompat(deltaX, deltaY, getScrollX(), getScrollY(), rangeX, rangeY, 0, + 0, true) && !hasNestedScrollingParent(ViewCompat.TYPE_TOUCH)) { + // Break our velocity if we hit a scroll barrier. + mVelocityTracker.clear(); + } + + final int scrolledDeltaX = getScrollX() - oldX; + final int scrolledDeltaY = getScrollY() - oldY; + final int unconsumedX = deltaX - scrolledDeltaX; + final int unconsumedY = deltaY - scrolledDeltaY; + if (dispatchNestedScroll(scrolledDeltaX, scrolledDeltaY, unconsumedX, unconsumedY, mScrollOffset, + ViewCompat.TYPE_TOUCH)) { + mLastMotionX -= mScrollOffset[0]; + mLastMotionY -= mScrollOffset[1]; + vtev.offsetLocation(mScrollOffset[0], mScrollOffset[1]); + mNestedXOffset += mScrollOffset[0]; + mNestedYOffset += mScrollOffset[1]; + } else if (canOverscroll) { + ensureGlows(); + final int pulledToX = oldX + deltaX; + final int pulledToY = oldY + deltaY; + if (pulledToX < 0) { + EdgeEffectCompat.onPull(mEdgeGlowLeft, (float) deltaX / getWidth(), + ev.getY(activePointerIndex) / getHeight()); + if (!mEdgeGlowRight.isFinished()) { + mEdgeGlowRight.onRelease(); + } + } else if (pulledToX > rangeX) { + EdgeEffectCompat.onPull(mEdgeGlowRight, (float) deltaX / getWidth(), + 1.f - ev.getY(activePointerIndex) + / getHeight()); + if (!mEdgeGlowLeft.isFinished()) { + mEdgeGlowLeft.onRelease(); + } + } + if (pulledToY < 0) { + EdgeEffectCompat.onPull(mEdgeGlowTop, (float) deltaY / getHeight(), + ev.getX(activePointerIndex) / getWidth()); + if (!mEdgeGlowBottom.isFinished()) { + mEdgeGlowBottom.onRelease(); + } + } else if (pulledToY > rangeY) { + EdgeEffectCompat.onPull(mEdgeGlowBottom, (float) deltaY / getHeight(), + 1.f - ev.getX(activePointerIndex) + / getWidth()); + if (!mEdgeGlowTop.isFinished()) { + mEdgeGlowTop.onRelease(); + } + } + if (mEdgeGlowTop != null + && (!mEdgeGlowTop.isFinished() || !mEdgeGlowBottom.isFinished() + || !mEdgeGlowLeft.isFinished() || !mEdgeGlowRight.isFinished())) { + ViewCompat.postInvalidateOnAnimation(this); + } + } + } + break; + case MotionEvent.ACTION_UP: + final VelocityTracker velocityTracker = mVelocityTracker; + velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); + int initialVelocityX = (int) velocityTracker.getXVelocity(mActivePointerId); + int initialVelocityY = (int) velocityTracker.getYVelocity(mActivePointerId); + if ((Math.abs(initialVelocityX) > mMinimumVelocity) || (Math.abs(initialVelocityY) > mMinimumVelocity)) { + flingWithNestedDispatch(-initialVelocityX, -initialVelocityY); + } else if (mScroller.springBack(getScrollX(), getScrollY(), 0, getScrollRangeX(), 0, + getScrollRangeY())) { + ViewCompat.postInvalidateOnAnimation(this); + } + mActivePointerId = INVALID_POINTER; + endDrag(); + break; + case MotionEvent.ACTION_CANCEL: + if (mIsBeingDragged && getChildCount() > 0) { + if (mScroller.springBack(getScrollX(), getScrollY(), 0, getScrollRangeX(), 0, + getScrollRangeY())) { + ViewCompat.postInvalidateOnAnimation(this); + } + } + mActivePointerId = INVALID_POINTER; + endDrag(); + break; + case MotionEvent.ACTION_POINTER_DOWN: { + final int index = ev.getActionIndex(); + mLastMotionX = (int) ev.getX(index); + mLastMotionY = (int) ev.getY(index); + mActivePointerId = ev.getPointerId(index); + break; + } + case MotionEvent.ACTION_POINTER_UP: + onSecondaryPointerUp(ev); + mLastMotionX = (int) ev.getX(ev.findPointerIndex(mActivePointerId)); + mLastMotionY = (int) ev.getY(ev.findPointerIndex(mActivePointerId)); + break; + } + + if (mVelocityTracker != null) { + mVelocityTracker.addMovement(vtev); + } + vtev.recycle(); + return true; + } + + private int getAxes() { + int axes = ViewCompat.SCROLL_AXIS_NONE; + if ((scrollModeFlag & DISABLE_VERTICAL_SCROLL) != DISABLE_VERTICAL_SCROLL) { + axes |= ViewCompat.SCROLL_AXIS_VERTICAL; + } + if ((scrollModeFlag & DISABLE_HORIZONTAL_SCROLL) != DISABLE_HORIZONTAL_SCROLL) { + axes |= ViewCompat.SCROLL_AXIS_HORIZONTAL; + } + return axes; + } + + private void onSecondaryPointerUp(MotionEvent ev) { + final int pointerIndex = ev.getActionIndex(); + final int pointerId = ev.getPointerId(pointerIndex); + if (pointerId == mActivePointerId) { + // This was our active pointer going up. Choose a new + // active pointer and adjust accordingly. + // TODO: Make this decision more intelligent. + final int newPointerIndex = pointerIndex == 0 ? 1 : 0; + mLastMotionY = (int) ev.getY(newPointerIndex); + mLastMotionX = (int) ev.getX(newPointerIndex); + mActivePointerId = ev.getPointerId(newPointerIndex); + if (mVelocityTracker != null) { + mVelocityTracker.clear(); + } + } + } + + @Override + public boolean onGenericMotionEvent(MotionEvent event) { + if ((event.getSource() & InputDeviceCompat.SOURCE_CLASS_POINTER) != 0) { + switch (event.getAction()) { + case MotionEvent.ACTION_SCROLL: { + if (!mIsBeingDragged) { + final float hscroll = event.getAxisValue(MotionEvent.AXIS_HSCROLL); + final float vscroll = event.getAxisValue(MotionEvent.AXIS_VSCROLL); + if (vscroll != 0 || hscroll != 0) { + final int deltaX = (int) (hscroll * getHorizontalScrollFactorCompat()); + final int deltaY = (int) (vscroll * getVerticalScrollFactorCompat()); + final int rangeX = getScrollRangeX(); + final int rangeY = getScrollRangeY(); + int oldScrollX = getScrollX(); + int oldScrollY = getScrollY(); + int newScrollX = Math.max(0, Math.min(rangeX, oldScrollX - deltaX)); + int newScrollY = Math.max(0, Math.min(rangeY, oldScrollY - deltaY)); + if (newScrollY != oldScrollY || newScrollX != oldScrollX) { + super.scrollTo(newScrollX, newScrollY); + return true; + } + } + } + } + } + } + return false; + } + + private float getHorizontalScrollFactorCompat() { + if (mHorizontalScrollFactor == 0) { + TypedValue outValue = new TypedValue(); + final Context context = getContext(); + if (!context.getTheme().resolveAttribute( + android.R.attr.listPreferredItemHeight, outValue, true)) { + throw new IllegalStateException( + "Expected theme to define listPreferredItemHeight."); + } + mHorizontalScrollFactor = outValue.getDimension( + context.getResources().getDisplayMetrics()); + } + return mHorizontalScrollFactor; + } + + private float getVerticalScrollFactorCompat() { + if (mVerticalScrollFactor == 0) { + TypedValue outValue = new TypedValue(); + final Context context = getContext(); + if (!context.getTheme().resolveAttribute( + android.R.attr.listPreferredItemHeight, outValue, true)) { + throw new IllegalStateException( + "Expected theme to define listPreferredItemHeight."); + } + mVerticalScrollFactor = outValue.getDimension( + context.getResources().getDisplayMetrics()); + } + return mVerticalScrollFactor; + } + + @Override + protected void onOverScrolled(int scrollX, int scrollY, + boolean clampedX, boolean clampedY) { + super.scrollTo(scrollX, scrollY); + } + + boolean overScrollByCompat(int deltaX, int deltaY, + int scrollX, int scrollY, + int scrollRangeX, int scrollRangeY, + int maxOverScrollX, int maxOverScrollY, + boolean isTouchEvent) { + final int overScrollMode = getOverScrollMode(); + final boolean canScrollHorizontal = + computeHorizontalScrollRange() > computeHorizontalScrollExtent(); + final boolean canScrollVertical = + computeVerticalScrollRange() > computeVerticalScrollExtent(); + final boolean overScrollHorizontal = overScrollMode == View.OVER_SCROLL_ALWAYS + || (overScrollMode == View.OVER_SCROLL_IF_CONTENT_SCROLLS && canScrollHorizontal); + final boolean overScrollVertical = overScrollMode == View.OVER_SCROLL_ALWAYS + || (overScrollMode == View.OVER_SCROLL_IF_CONTENT_SCROLLS && canScrollVertical); + + int newScrollX = scrollX + deltaX; + if (!overScrollHorizontal) { + maxOverScrollX = 0; + } + + int newScrollY = scrollY + deltaY; + if (!overScrollVertical) { + maxOverScrollY = 0; + } + + // Clamp values if at the limits and record + final int left = -maxOverScrollX; + final int right = maxOverScrollX + scrollRangeX; + final int top = -maxOverScrollY; + final int bottom = maxOverScrollY + scrollRangeY; + + boolean clampedX = false; + if (newScrollX > right) { + newScrollX = right; + clampedX = true; + } else if (newScrollX < left) { + newScrollX = left; + clampedX = true; + } + + boolean clampedY = false; + if (newScrollY > bottom) { + newScrollY = bottom; + clampedY = true; + } else if (newScrollY < top) { + newScrollY = top; + clampedY = true; + } + + if ((clampedX || clampedY) && !hasNestedScrollingParent(ViewCompat.TYPE_NON_TOUCH)) { + mScroller.springBack(newScrollX, newScrollY, 0, getScrollRangeX(), 0, getScrollRangeY()); + } + + onOverScrolled(newScrollX, newScrollY, clampedX, clampedY); + + return clampedX || clampedY; + } + + int getScrollRangeX() { + int scrollRange = 0; + if (getChildCount() > 0) { + View child = getChildAt(0); + scrollRange = Math.max(0, + child.getWidth() - (getWidth() - getPaddingLeft() - getPaddingRight())); + } + return scrollRange; + } + + int getScrollRangeY() { + int scrollRange = 0; + if (getChildCount() > 0) { + View child = getChildAt(0); + scrollRange = Math.max(0, + child.getHeight() - (getHeight() - getPaddingBottom() - getPaddingTop())); + } + return scrollRange; + } + + /** + *

+ * Finds the next focusable component that fits in the specified bounds. + *

+ * + * @param topFocus look for a candidate is the one at the top of the bounds + * if topFocus is true, or at the bottom of the bounds if topFocus is + * false + * @param top the top offset of the bounds in which a focusable must be + * found + * @param bottom the bottom offset of the bounds in which a focusable must + * be found + * @return the next focusable component in the bounds or null if none can + * be found + */ + private View findFocusableViewInBounds(boolean topFocus, int top, int bottom) { + + List focusables = getFocusables(View.FOCUS_FORWARD); + View focusCandidate = null; + + /* + * A fully contained focusable is one where its top is below the bound's + * top, and its bottom is above the bound's bottom. A partially + * contained focusable is one where some part of it is within the + * bounds, but it also has some part that is not within bounds. A fully contained + * focusable is preferred to a partially contained focusable. + */ + boolean foundFullyContainedFocusable = false; + + int count = focusables.size(); + for (int i = 0; i < count; i++) { + View view = focusables.get(i); + int viewTop = view.getTop(); + int viewBottom = view.getBottom(); + + if (top < viewBottom && viewTop < bottom) { + /* + * the focusable is in the target area, it is a candidate for + * focusing + */ + + final boolean viewIsFullyContained = (top < viewTop) && (viewBottom < bottom); + + if (focusCandidate == null) { + /* No candidate, take this one */ + focusCandidate = view; + foundFullyContainedFocusable = viewIsFullyContained; + } else { + final boolean viewIsCloserToBoundary = + (topFocus && viewTop < focusCandidate.getTop()) + || (!topFocus && viewBottom > focusCandidate.getBottom()); + + if (foundFullyContainedFocusable) { + if (viewIsFullyContained && viewIsCloserToBoundary) { + /* + * We're dealing with only fully contained views, so + * it has to be closer to the boundary to beat our + * candidate + */ + focusCandidate = view; + } + } else { + if (viewIsFullyContained) { + /* Any fully contained view beats a partially contained view */ + focusCandidate = view; + foundFullyContainedFocusable = true; + } else if (viewIsCloserToBoundary) { + /* + * Partially contained view beats another partially + * contained view if it's closer + */ + focusCandidate = view; + } + } + } + } + } + + return focusCandidate; + } + + /** + *

Handles scrolling in response to a "page up/down" shortcut press. This + * method will scroll the view by one page up or down and give the focus + * to the topmost/bottommost component in the new visible area. If no + * component is a good candidate for focus, this scrollview reclaims the + * focus.

+ * + * @param direction the scroll direction: {@link android.view.View#FOCUS_UP} + * to go one page up or + * {@link android.view.View#FOCUS_DOWN} to go one page down + * @return true if the key event is consumed by this method, false otherwise + */ + public boolean pageScroll(int direction) { + boolean down = direction == View.FOCUS_DOWN; + int height = getHeight(); + + if (down) { + mTempRect.top = getScrollY() + height; + int count = getChildCount(); + if (count > 0) { + View view = getChildAt(count - 1); + if (mTempRect.top + height > view.getBottom()) { + mTempRect.top = view.getBottom() - height; + } + } + } else { + mTempRect.top = getScrollY() - height; + if (mTempRect.top < 0) { + mTempRect.top = 0; + } + } + mTempRect.bottom = mTempRect.top + height; + + return scrollAndFocus(direction, mTempRect.top, mTempRect.bottom); + } + + /** + *

Handles scrolling in response to a "home/end" shortcut press. This + * method will scroll the view to the top or bottom and give the focus + * to the topmost/bottommost component in the new visible area. If no + * component is a good candidate for focus, this scrollview reclaims the + * focus.

+ * + * @param direction the scroll direction: {@link android.view.View#FOCUS_UP} + * to go the top of the view or + * {@link android.view.View#FOCUS_DOWN} to go the bottom + * @return true if the key event is consumed by this method, false otherwise + */ + public boolean fullScroll(int direction) { + boolean down = direction == View.FOCUS_DOWN; + int height = getHeight(); + + mTempRect.top = 0; + mTempRect.bottom = height; + + if (down) { + int count = getChildCount(); + if (count > 0) { + View view = getChildAt(count - 1); + mTempRect.bottom = view.getBottom() + getPaddingBottom(); + mTempRect.top = mTempRect.bottom - height; + } + } + + return scrollAndFocus(direction, mTempRect.top, mTempRect.bottom); + } + + /** + *

Scrolls the view to make the area defined by top and + * bottom visible. This method attempts to give the focus + * to a component visible in this area. If no component can be focused in + * the new visible area, the focus is reclaimed by this ScrollView.

+ * + * @param direction the scroll direction: {@link android.view.View#FOCUS_UP} + * to go upward, {@link android.view.View#FOCUS_DOWN} to downward + * @param top the top offset of the new area to be made visible + * @param bottom the bottom offset of the new area to be made visible + * @return true if the key event is consumed by this method, false otherwise + */ + private boolean scrollAndFocus(int direction, int top, int bottom) { + boolean handled = true; + + int height = getHeight(); + int containerTop = getScrollY(); + int containerBottom = containerTop + height; + boolean up = direction == View.FOCUS_UP; + + View newFocused = findFocusableViewInBounds(up, top, bottom); + if (newFocused == null) { + newFocused = this; + } + + if (top >= containerTop && bottom <= containerBottom) { + handled = false; + } else { + int delta = up ? (top - containerTop) : (bottom - containerBottom); + doScroll(new int[]{0, delta}); + } + + if (newFocused != findFocus()) newFocused.requestFocus(direction); + + return handled; + } + + /** + * Handle scrolling in response to an up or down arrow click. + * + * @param direction The direction corresponding to the arrow key that was + * pressed + * @return True if we consumed the event, false otherwise + */ + public boolean arrowScroll(int direction) { + + View currentFocused = findFocus(); + if (currentFocused == this) currentFocused = null; + + View nextFocused = FocusFinder.getInstance().findNextFocus(this, currentFocused, direction); + + final int maxJump = getMaxScrollAmount(); + + if (nextFocused != null && isWithinDeltaOfScreen(nextFocused, maxJump, getHeight())) { + nextFocused.getDrawingRect(mTempRect); + offsetDescendantRectToMyCoords(nextFocused, mTempRect); + int[] scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect); + doScroll(scrollDelta); + nextFocused.requestFocus(direction); + } else { + // no new focus + int scrollDelta = maxJump; + + if (direction == View.FOCUS_UP && getScrollY() < scrollDelta) { + scrollDelta = getScrollY(); + } else if (direction == View.FOCUS_DOWN) { + if (getChildCount() > 0) { + int daBottom = getChildAt(0).getBottom(); + int screenBottom = getScrollY() + getHeight() - getPaddingBottom(); + if (daBottom - screenBottom < maxJump) { + scrollDelta = daBottom - screenBottom; + } + } + } + if (scrollDelta == 0) { + return false; + } + doScroll(new int[]{0, direction == View.FOCUS_DOWN ? scrollDelta : -scrollDelta}); + } + + if (currentFocused != null && currentFocused.isFocused() + && isOffScreen(currentFocused)) { + // previously focused item still has focus and is off screen, give + // it up (take it back to ourselves) + // (also, need to temporarily force FOCUS_BEFORE_DESCENDANTS so we are + // sure to + // get it) + final int descendantFocusability = getDescendantFocusability(); // save + setDescendantFocusability(ViewGroup.FOCUS_BEFORE_DESCENDANTS); + requestFocus(); + setDescendantFocusability(descendantFocusability); // restore + } + return true; + } + + /** + * @return whether the descendant of this scroll view is scrolled off + * screen. + */ + private boolean isOffScreen(View descendant) { + return !isWithinDeltaOfScreen(descendant, 0, getHeight()); + } + + /** + * @return whether the descendant of this scroll view is within delta + * pixels of being on the screen. + */ + private boolean isWithinDeltaOfScreen(View descendant, int delta, int height) { + descendant.getDrawingRect(mTempRect); + offsetDescendantRectToMyCoords(descendant, mTempRect); + + return (mTempRect.bottom + delta) >= getScrollY() + && (mTempRect.top - delta) <= (getScrollY() + height); + } + + /** + * Smooth scroll + * + * @param delta the number of pixels to scroll by on the X or Y axis + */ + private void doScroll(int[] delta) { + if (delta[0] != 0 || delta[1] != 0) { + if (mSmoothScrollingEnabled) { + smoothScrollBy(delta[0], delta[1]); + } else { + scrollBy(delta[0], delta[1]); + } + } + } + + /** + * Like {@link View#scrollBy}, but scroll smoothly instead of immediately. + * + * @param dx the number of pixels to scroll by on the X axis + * @param dy the number of pixels to scroll by on the Y axis + */ + public final void smoothScrollBy(int dx, int dy) { + if (getChildCount() == 0) { + // Nothing to do. + return; + } + long duration = AnimationUtils.currentAnimationTimeMillis() - mLastScroll; + if (duration > ANIMATED_SCROLL_GAP) { + final int width = getWidth() - getPaddingLeft() - getPaddingRight(); + final int height = getHeight() - getPaddingBottom() - getPaddingTop(); + final int right = getChildAt(0).getWidth(); + final int bottom = getChildAt(0).getHeight(); + final int maxX = Math.max(0, right - width); + final int maxY = Math.max(0, bottom - height); + final int scrollX = getScrollX(); + final int scrollY = getScrollY(); + dx = Math.max(0, Math.min(scrollX + dx, maxX)) - scrollX; + dy = Math.max(0, Math.min(scrollY + dy, maxY)) - scrollY; + mScroller.startScroll(scrollX, scrollY, dx, dy); + ViewCompat.postInvalidateOnAnimation(this); + } else { + if (!mScroller.isFinished()) { + mScroller.abortAnimation(); + } + scrollBy(dx, dy); + } + mLastScroll = AnimationUtils.currentAnimationTimeMillis(); + } + + /** + * Like {@link #scrollTo}, but scroll smoothly instead of immediately. + * + * @param x the position where to scroll on the X axis + * @param y the position where to scroll on the Y axis + */ + public final void smoothScrollTo(int x, int y) { + smoothScrollBy(x - getScrollX(), y - getScrollY()); + } + + /** + *

The scroll range of a scroll view is the overall height of all of its + * children.

+ * + * @hide + */ + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + @Override + public int computeVerticalScrollRange() { + final int count = getChildCount(); + final int contentHeight = getHeight() - getPaddingBottom() - getPaddingTop(); + if (count == 0) { + return contentHeight; + } + + int scrollRange = getChildAt(0).getBottom(); + final int scrollY = getScrollY(); + final int overscrollBottom = Math.max(0, scrollRange - contentHeight); + if (scrollY < 0) { + scrollRange -= scrollY; + } else if (scrollY > overscrollBottom) { + scrollRange += scrollY - overscrollBottom; + } + + return scrollRange; + } + + /** + * @hide + */ + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + @Override + public int computeVerticalScrollOffset() { + return Math.max(0, super.computeVerticalScrollOffset()); + } + + /** + * @hide + */ + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + @Override + public int computeVerticalScrollExtent() { + return super.computeVerticalScrollExtent(); + } + + /** + * @hide + */ + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + @Override + public int computeHorizontalScrollRange() { + final int count = getChildCount(); + final int contentWidth = getWidth() - getPaddingLeft() - getPaddingRight(); + if (count == 0) { + return contentWidth; + } + + int scrollRange = getChildAt(0).getRight(); + final int scrollX = getScrollX(); + final int overscrollRight = Math.max(0, scrollRange - contentWidth); + if (scrollX < 0) { + scrollRange -= scrollX; + } else if (scrollX > overscrollRight) { + scrollRange += scrollX - overscrollRight; + } + + return scrollRange; + } + + /** + * @hide + */ + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + @Override + public int computeHorizontalScrollOffset() { + return super.computeHorizontalScrollOffset(); + } + + /** + * @hide + */ + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + @Override + public int computeHorizontalScrollExtent() { + return super.computeHorizontalScrollExtent(); + } + + @Override + protected void measureChild(View child, int parentWidthMeasureSpec, + int parentHeightMeasureSpec) { + ViewGroup.LayoutParams lp = child.getLayoutParams(); + + int childWidthMeasureSpec; + int childHeightMeasureSpec; + + childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); + + childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); + + child.measure(childWidthMeasureSpec, childHeightMeasureSpec); + } + + @Override + protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, + int parentHeightMeasureSpec, int heightUsed) { + final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); + + final int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec( + lp.leftMargin + lp.rightMargin, MeasureSpec.UNSPECIFIED); + final int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec( + lp.topMargin + lp.bottomMargin, MeasureSpec.UNSPECIFIED); + + child.measure(childWidthMeasureSpec, childHeightMeasureSpec); + } + + @Override + public void computeScroll() { + if (mScroller.computeScrollOffset()) { + final int x = mScroller.getCurrX(); + final int y = mScroller.getCurrY(); + + int dx = x - mLastScrollerX; + int dy = y - mLastScrollerY; + + // Dispatch up to parent + if (dispatchNestedPreScroll(dx, dy, mScrollConsumed, null, ViewCompat.TYPE_NON_TOUCH)) { + dx -= mScrollConsumed[0]; + dy -= mScrollConsumed[1]; + } + + if (dy != 0 || dx != 0) { + final int rangeX = getScrollRangeX(); + final int rangeY = getScrollRangeY(); + final int oldScrollX = getScrollX(); + final int oldScrollY = getScrollY(); + + overScrollByCompat(dx, dy, oldScrollX, oldScrollY, rangeX, rangeY, 0, 0, false); + + final int scrolledDeltaX = getScrollX() - oldScrollX; + final int scrolledDeltaY = getScrollY() - oldScrollY; + final int unconsumedX = dx - scrolledDeltaX; + final int unconsumedY = dy - scrolledDeltaY; + + if (!dispatchNestedScroll(scrolledDeltaX, scrolledDeltaY, unconsumedX, unconsumedY, null, + ViewCompat.TYPE_NON_TOUCH)) { + final int mode = getOverScrollMode(); + final boolean canOverscroll = mode == OVER_SCROLL_ALWAYS + || (mode == OVER_SCROLL_IF_CONTENT_SCROLLS && (rangeX > 0 || rangeY > 0)); + if (canOverscroll) { + ensureGlows(); + if (x <= 0 && oldScrollX > 0) { + mEdgeGlowLeft.onAbsorb((int) mScroller.getCurrVelocity()); + } else if (x >= rangeX && oldScrollX < rangeX) { + mEdgeGlowRight.onAbsorb((int) mScroller.getCurrVelocity()); + } + if (y <= 0 && oldScrollY > 0) { + mEdgeGlowTop.onAbsorb((int) mScroller.getCurrVelocity()); + } else if (y >= rangeY && oldScrollY < rangeY) { + mEdgeGlowBottom.onAbsorb((int) mScroller.getCurrVelocity()); + } + } + } + } + + // Finally update the scroll positions and post an invalidation + mLastScrollerX = x; + mLastScrollerY = y; + ViewCompat.postInvalidateOnAnimation(this); + } else { + // We can't scroll any more, so stop any indirect scrolling + if (hasNestedScrollingParent(ViewCompat.TYPE_NON_TOUCH)) { + stopNestedScroll(ViewCompat.TYPE_NON_TOUCH); + } + // and reset the scroller y + mLastScrollerX = 0; + mLastScrollerY = 0; + } + } + + /** + * Scrolls the view to the given child. + * + * @param child the View to scroll to + */ + private void scrollToChild(View child) { + child.getDrawingRect(mTempRect); + + /* Offset from child's local coordinates to ScrollView coordinates */ + offsetDescendantRectToMyCoords(child, mTempRect); + + int[] scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect); + + if (scrollDelta[0] != 0 || scrollDelta[1] != 0) { + scrollBy(scrollDelta[0], scrollDelta[1]); + } + } + + /** + * If rect is off screen, scroll just enough to get it (or at least the + * first screen size chunk of it) on screen. + * + * @param rect The rectangle. + * @param immediate True to scroll immediately without animation + * @return true if scrolling was performed + */ + private boolean scrollToChildRect(Rect rect, boolean immediate) { + final int[] delta = computeScrollDeltaToGetChildRectOnScreen(rect); + final boolean scroll = delta[0] != 0 || delta[1] != 0; + if (scroll) { + if (immediate) { + scrollBy(delta[0], delta[1]); + } else { + smoothScrollBy(delta[0], delta[1]); + } + } + return scroll; + } + + /** + * Compute the amount to scroll in the Y direction in order to get + * a rectangle completely on the screen (or, if taller than the screen, + * at least the first screen size chunk of it). + * + * @param rect The rect. + * @return The scroll delta. + */ + protected int[] computeScrollDeltaToGetChildRectOnScreen(Rect rect) { + if (getChildCount() == 0) return new int[]{0, 0}; + + int width = getWidth(); + int height = getHeight(); + int screenTop = getScrollY(); + int screenLeft = getScrollX(); + int screenRight = screenLeft + width; + int screenBottom = screenTop + height; + + int fadingEdgeX = getHorizontalFadingEdgeLength(); + int fadingEdgeY = getVerticalFadingEdgeLength(); + + // leave room for top fading edge as long as rect isn't at very top + if (rect.top > 0) { + screenTop += fadingEdgeY; + } + if (rect.left > 0) { + screenLeft += fadingEdgeX; + } + if (rect.right < getChildAt(0).getHeight()) { + screenRight -= fadingEdgeX; + } + // leave room for bottom fading edge as long as rect isn't at very bottom + if (rect.bottom < getChildAt(0).getHeight()) { + screenBottom -= fadingEdgeY; + } + + int scrollXDelta = 0; + int scrollYDelta = 0; + if (rect.right > screenRight && rect.left > screenLeft) { + // need to move down to get it in view: move down just enough so + // that the entire rectangle is in view (or at least the first + // screen size chunk). + + if (rect.width() > width) { + // just enough to get screen size chunk on + scrollXDelta += (rect.left - screenLeft); + } else { + // get entire rect at bottom of screen + scrollXDelta += (rect.right - screenRight); + } + + // make sure we aren't scrolling beyond the end of our content + int right = getChildAt(0).getRight(); + int distanceToRight = right - screenRight; + scrollXDelta = Math.min(scrollXDelta, distanceToRight); + } else if (rect.left < screenLeft && rect.right < screenRight) { + // need to move up to get it in view: move up just enough so that + // entire rectangle is in view (or at least the first screen + // size chunk of it). + + if (rect.width() > width) { + // screen size chunk + scrollXDelta -= (screenRight - rect.right); + } else { + // entire rect at top + scrollXDelta -= (screenLeft - rect.left); + } + + // make sure we aren't scrolling any further than the top our content + scrollXDelta = Math.max(scrollXDelta, -getScrollX()); + } + if (rect.bottom > screenBottom && rect.top > screenTop) { + // need to move down to get it in view: move down just enough so + // that the entire rectangle is in view (or at least the first + // screen size chunk). + + if (rect.height() > height) { + // just enough to get screen size chunk on + scrollYDelta += (rect.top - screenTop); + } else { + // get entire rect at bottom of screen + scrollYDelta += (rect.bottom - screenBottom); + } + + // make sure we aren't scrolling beyond the end of our content + int bottom = getChildAt(0).getBottom(); + int distanceToBottom = bottom - screenBottom; + scrollYDelta = Math.min(scrollYDelta, distanceToBottom); + + } else if (rect.top < screenTop && rect.bottom < screenBottom) { + // need to move up to get it in view: move up just enough so that + // entire rectangle is in view (or at least the first screen + // size chunk of it). + + if (rect.height() > height) { + // screen size chunk + scrollYDelta -= (screenBottom - rect.bottom); + } else { + // entire rect at top + scrollYDelta -= (screenTop - rect.top); + } + + // make sure we aren't scrolling any further than the top our content + scrollYDelta = Math.max(scrollYDelta, -getScrollY()); + } + return new int[]{scrollXDelta, scrollYDelta}; + } + + @Override + public void requestChildFocus(View child, View focused) { + if (!mIsLayoutDirty) { + scrollToChild(focused); + } else { + // The child may not be laid out yet, we can't compute the scroll yet + mChildToScrollTo = focused; + } + super.requestChildFocus(child, focused); + } + + + /** + * When looking for focus in children of a scroll view, need to be a little + * more careful not to give focus to something that is scrolled off screen. + *

+ * This is more expensive than the default {@link android.view.ViewGroup} + * implementation, otherwise this behavior might have been made the default. + */ + @Override + protected boolean onRequestFocusInDescendants(int direction, + Rect previouslyFocusedRect) { + + // convert from forward / backward notation to up / down / left / right + // (ugh). + if (direction == View.FOCUS_FORWARD) { + direction = View.FOCUS_DOWN; + } else if (direction == View.FOCUS_BACKWARD) { + direction = View.FOCUS_UP; + } + + final View nextFocus = previouslyFocusedRect == null + ? FocusFinder.getInstance().findNextFocus(this, null, direction) + : FocusFinder.getInstance().findNextFocusFromRect( + this, previouslyFocusedRect, direction); + + if (nextFocus == null) { + return false; + } + + if (isOffScreen(nextFocus)) { + return false; + } + + return nextFocus.requestFocus(direction, previouslyFocusedRect); + } + + @Override + public boolean requestChildRectangleOnScreen(View child, Rect rectangle, + boolean immediate) { + // offset into coordinate space of this scroll view + rectangle.offset(child.getLeft() - child.getScrollX(), + child.getTop() - child.getScrollY()); + + return scrollToChildRect(rectangle, immediate); + } + + @Override + public void requestLayout() { + mIsLayoutDirty = true; + super.requestLayout(); + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + super.onLayout(changed, l, t, r, b); + mIsLayoutDirty = false; + // Give a child focus if it needs it + if (mChildToScrollTo != null && isViewDescendantOf(mChildToScrollTo, this)) { + scrollToChild(mChildToScrollTo); + } + mChildToScrollTo = null; + + if (!mIsLaidOut) { + if (mSavedState != null) { + scrollTo(mSavedState.scrollPositionX, mSavedState.scrollPositionY); + mSavedState = null; + } // mScrollY default value is "0" + + final int childWidth = (getChildCount() > 0) ? getChildAt(0).getMeasuredWidth() : 0; + final int childHeight = (getChildCount() > 0) ? getChildAt(0).getMeasuredHeight() : 0; + final int scrollRangeX = Math.max(0, + childWidth - (b - t - getPaddingLeft() - getPaddingRight())); + final int scrollRangeY = Math.max(0, + childHeight - (b - t - getPaddingBottom() - getPaddingTop())); + + // Don't forget to clamp + int scrollX = Math.max(0, Math.min(scrollRangeX, getScrollX())); + int scrollY = Math.max(0, Math.min(scrollRangeY, getScrollY())); + scrollTo(scrollX, scrollY); + } + + // Calling this with the present values causes it to re-claim them + scrollTo(getScrollX(), getScrollY()); + mIsLaidOut = true; + } + + @Override + public void onAttachedToWindow() { + super.onAttachedToWindow(); + + mIsLaidOut = false; + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + super.onSizeChanged(w, h, oldw, oldh); + + View currentFocused = findFocus(); + if (null == currentFocused || this == currentFocused) { + return; + } + + // If the currently-focused view was visible on the screen when the + // screen was at the old height, then scroll the screen to make that + // view visible with the new screen height. + if (isWithinDeltaOfScreen(currentFocused, oldw, oldh)) { + currentFocused.getDrawingRect(mTempRect); + offsetDescendantRectToMyCoords(currentFocused, mTempRect); + int[] scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect); + doScroll(scrollDelta); + } + } + + /** + * Return true if child is a descendant of parent, (or equal to the parent). + */ + private static boolean isViewDescendantOf(View child, View parent) { + if (child == parent) { + return true; + } + + final ViewParent theParent = child.getParent(); + return (theParent instanceof ViewGroup) && isViewDescendantOf((View) theParent, parent); + } + + /** + * Fling the scroll view + * + * @param velocityX The initial velocity in the X direction. Positive + * numbers mean that the finger/cursor is moving right to the screen, + * which means we want to scroll towards the left. + * @param velocityY The initial velocity in the Y direction. Positive + * numbers mean that the finger/cursor is moving down the screen, + * which means we want to scroll towards the top. + */ + public void fling(int velocityX, int velocityY) { + if (getChildCount() > 0) { + int axes = getAxes(); + startNestedScroll(axes, ViewCompat.TYPE_NON_TOUCH); + mScroller.fling(getScrollX(), getScrollY(), // start + velocityX, velocityY, // velocities + Integer.MIN_VALUE, Integer.MAX_VALUE, // x + Integer.MIN_VALUE, Integer.MAX_VALUE, // y + 0, 0); // overscroll + mLastScrollerX = getScrollX(); + mLastScrollerY = getScrollY(); + ViewCompat.postInvalidateOnAnimation(this); + } + } + + private void flingWithNestedDispatch(int velocityX, int velocityY) { + final int scrollX = getScrollX(); + final int scrollY = getScrollY(); + final boolean canFling = ((scrollY > 0 || velocityY > 0) + && (scrollY < getScrollRangeY() || velocityY < 0)) + || ((scrollX > 0 || velocityX > 0) + && (scrollX < getScrollRangeX() || velocityX < 0)); + if (!dispatchNestedPreFling(velocityX, velocityY)) { + dispatchNestedFling(velocityX, velocityY, canFling); + fling(velocityX, velocityY); + } + + } + + private void endDrag() { + mIsBeingDragged = false; + + recycleVelocityTracker(); + stopNestedScroll(ViewCompat.TYPE_TOUCH); + + if (mEdgeGlowTop != null) { + mEdgeGlowTop.onRelease(); + mEdgeGlowBottom.onRelease(); + mEdgeGlowLeft.onRelease(); + mEdgeGlowRight.onRelease(); + } + } + + /** + * {@inheritDoc} + *

+ *

This version also clamps the scrolling to the bounds of our child. + */ + @Override + public void scrollTo(int x, int y) { + // we rely on the fact the View.scrollBy calls scrollTo. + if (getChildCount() > 0) { + View child = getChildAt(0); + x = clamp(x, getWidth() - getPaddingRight() - getPaddingLeft(), child.getWidth()); + y = clamp(y, getHeight() - getPaddingBottom() - getPaddingTop(), child.getHeight()); + if (x != getScrollX() || y != getScrollY()) { + super.scrollTo(x, y); + } + } + } + + private void ensureGlows() { + if (getOverScrollMode() != View.OVER_SCROLL_NEVER) { + if (mEdgeGlowTop == null) { + Context context = getContext(); + mEdgeGlowTop = new EdgeEffect(context); + mEdgeGlowBottom = new EdgeEffect(context); + mEdgeGlowLeft = new EdgeEffect(context); + mEdgeGlowRight = new EdgeEffect(context); + } + } else { + mEdgeGlowTop = null; + mEdgeGlowBottom = null; + mEdgeGlowLeft = null; + mEdgeGlowRight = null; + } + } + + @Override + public void draw(Canvas canvas) { + super.draw(canvas); + if (mEdgeGlowTop != null) { + final int scrollX = getScrollX(); + final int scrollY = getScrollY(); + if (!mEdgeGlowTop.isFinished()) { + final int restoreCount = canvas.save(); + final int width = getWidth() - getPaddingLeft() - getPaddingRight(); + + canvas.translate(getPaddingLeft(), Math.min(0, scrollY)); + mEdgeGlowTop.setSize(width, getHeight()); + if (mEdgeGlowTop.draw(canvas)) { + ViewCompat.postInvalidateOnAnimation(this); + } + canvas.restoreToCount(restoreCount); + } + if (!mEdgeGlowLeft.isFinished()) { + final int restoreCount = canvas.save(); + final int height = getHeight() - getPaddingTop() - getPaddingBottom(); + + canvas.translate(getPaddingTop(), Math.min(0, scrollX)); + mEdgeGlowLeft.setSize(getWidth(), height); + if (mEdgeGlowLeft.draw(canvas)) { + ViewCompat.postInvalidateOnAnimation(this); + } + canvas.restoreToCount(restoreCount); + } + if (!mEdgeGlowRight.isFinished()) { + final int restoreCount = canvas.save(); + final int width = getWidth(); + final int height = getHeight() - getPaddingTop() - getPaddingBottom(); + + canvas.translate(Math.max(getScrollRangeX(), scrollX) + width, -height + getPaddingTop()); + canvas.rotate(180, 0, height); + mEdgeGlowBottom.setSize(width, height); + if (mEdgeGlowBottom.draw(canvas)) { + ViewCompat.postInvalidateOnAnimation(this); + } + canvas.restoreToCount(restoreCount); + } + if (!mEdgeGlowBottom.isFinished()) { + final int restoreCount = canvas.save(); + final int width = getWidth() - getPaddingLeft() - getPaddingRight(); + final int height = getHeight(); + + canvas.translate(-width + getPaddingLeft(), + Math.max(getScrollRangeY(), scrollY) + height); + canvas.rotate(180, width, 0); + mEdgeGlowBottom.setSize(width, height); + if (mEdgeGlowBottom.draw(canvas)) { + ViewCompat.postInvalidateOnAnimation(this); + } + canvas.restoreToCount(restoreCount); + } + } + } + + private static int clamp(int n, int my, int child) { + if (my >= child || n < 0) { + /* my >= child is this case: + * |--------------- me ---------------| + * |------ child ------| + * or + * |--------------- me ---------------| + * |------ child ------| + * or + * |--------------- me ---------------| + * |------ child ------| + * + * n < 0 is this case: + * |------ me ------| + * |-------- child --------| + * |-- mScrollX --| + */ + return 0; + } + if ((my + n) > child) { + /* this case: + * |------ me ------| + * |------ child ------| + * |-- mScrollX --| + */ + return child - my; + } + return n; + } + + @Override + protected void onRestoreInstanceState(Parcelable state) { + if (!(state instanceof SavedState)) { + super.onRestoreInstanceState(state); + return; + } + + SavedState ss = (SavedState) state; + super.onRestoreInstanceState(ss.getSuperState()); + mSavedState = ss; + requestLayout(); + } + + @Override + protected Parcelable onSaveInstanceState() { + Parcelable superState = super.onSaveInstanceState(); + SavedState ss = new SavedState(superState); + ss.scrollPositionX = getScrollX(); + ss.scrollPositionY = getScrollY(); + return ss; + } + + static class SavedState extends BaseSavedState { + public int scrollPositionX; + public int scrollPositionY; + + SavedState(Parcelable superState) { + super(superState); + } + + SavedState(Parcel source) { + super(source); + scrollPositionX = source.readInt(); + scrollPositionY = source.readInt(); + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + super.writeToParcel(dest, flags); + dest.writeInt(scrollPositionX); + dest.writeInt(scrollPositionY); + } + + @Override + public String toString() { + return "HorizontalScrollView.SavedState{" + + Integer.toHexString(System.identityHashCode(this)) + + " scrollPositionX=" + scrollPositionX + + ", scrollPositionY=" + scrollPositionY + + "}"; + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + @Override + public SavedState createFromParcel(Parcel in) { + return new SavedState(in); + } + + @Override + public SavedState[] newArray(int size) { + return new SavedState[size]; + } + }; + } + + static class AccessibilityDelegate extends AccessibilityDelegateCompat { + @Override + public boolean performAccessibilityAction(View host, int action, Bundle arguments) { + if (super.performAccessibilityAction(host, action, arguments)) { + return true; + } + final HVScrollView nsvHost = (HVScrollView) host; + if (!nsvHost.isEnabled()) { + return false; + } + switch (action) { + case AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD: { + final int viewportHeight = nsvHost.getHeight() - nsvHost.getPaddingBottom() + - nsvHost.getPaddingTop(); + final int targetScrollY = Math.min(nsvHost.getScrollY() + viewportHeight, + nsvHost.getScrollRangeY()); + if (targetScrollY != nsvHost.getScrollY()) { + nsvHost.smoothScrollTo(0, targetScrollY); + return true; + } + } + return false; + case AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD: { + final int viewportHeight = nsvHost.getHeight() - nsvHost.getPaddingBottom() + - nsvHost.getPaddingTop(); + final int targetScrollY = Math.max(nsvHost.getScrollY() - viewportHeight, 0); + if (targetScrollY != nsvHost.getScrollY()) { + nsvHost.smoothScrollTo(0, targetScrollY); + return true; + } + } + return false; + } + return false; + } + + @Override + public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) { + super.onInitializeAccessibilityNodeInfo(host, info); + final HVScrollView nsvHost = (HVScrollView) host; + info.setClassName(ScrollView.class.getName()); + if (nsvHost.isEnabled()) { + final int scrollRange = nsvHost.getScrollRangeY(); + if (scrollRange > 0) { + info.setScrollable(true); + if (nsvHost.getScrollY() > 0) { + info.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD); + } + if (nsvHost.getScrollY() < scrollRange) { + info.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD); + } + } + } + } + + @Override + public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(host, event); + final HVScrollView nsvHost = (HVScrollView) host; + event.setClassName(ScrollView.class.getName()); + final boolean scrollable = nsvHost.getScrollRangeY() > 0; + event.setScrollable(scrollable); + event.setScrollX(nsvHost.getScrollX()); + event.setScrollY(nsvHost.getScrollY()); + AccessibilityRecordCompat.setMaxScrollX(event, nsvHost.getScrollX()); + AccessibilityRecordCompat.setMaxScrollY(event, nsvHost.getScrollRangeY()); + } + } +} \ No newline at end of file diff --git a/demo/index.ts b/demo/index.ts index 852d019c..21a61315 100644 --- a/demo/index.ts +++ b/demo/index.ts @@ -2,4 +2,5 @@ export default [ 'src/Counter', 'src/Snake', 'src/ListDemo', + 'src/ScrollerDemo', ] \ No newline at end of file diff --git a/demo/src/ScrollerDemo.ts b/demo/src/ScrollerDemo.ts new file mode 100644 index 00000000..9373f322 --- /dev/null +++ b/demo/src/ScrollerDemo.ts @@ -0,0 +1,60 @@ +import { Group, Panel, List, text, gravity, Color, Stack, LayoutSpec, list, NativeCall, listItem, log, vlayout, Gravity, hlayout, scroller } from "doric"; +const colors = [ + "#f0932b", + "#eb4d4b", + "#6ab04c", + "#e056fd", + "#686de0", + "#30336b", +] +@Entry +class ScrollerPanel extends Panel { + build(rootView: Group): void { + rootView.addChild(scroller(vlayout( + [ + // ...[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5].map(e => text({ + // text: colors[e % colors.length], + // textColor: Color.parse('#ffffff'), + // textSize: 20, + // bgColor: Color.parse(colors[e % colors.length]), + // layoutConfig: { + // widthSpec: LayoutSpec.EXACTLY, + // heightSpec: LayoutSpec.EXACTLY, + // }, + // width: 200, + // height: 50, + // })), + ...[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5].map(i => hlayout([ + ...[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5].map(j => text({ + text: colors[(i + j) % colors.length], + textColor: Color.parse('#ffffff'), + textSize: 20, + bgColor: Color.parse(colors[(i + j) % colors.length]), + layoutConfig: { + widthSpec: LayoutSpec.EXACTLY, + heightSpec: LayoutSpec.EXACTLY, + }, + width: 80, + height: 50, + })), + ] + )), + hlayout([ + ...[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5].map(e => text({ + text: colors[e % colors.length], + textColor: Color.parse('#ffffff'), + textSize: 20, + bgColor: Color.parse(colors[e % colors.length]), + layoutConfig: { + widthSpec: LayoutSpec.EXACTLY, + heightSpec: LayoutSpec.EXACTLY, + }, + width: 200, + height: 50, + })), + ] + ), + ] + ))) + } +} \ No newline at end of file diff --git a/js-framework/index.ts b/js-framework/index.ts index ed664e67..8ad97435 100644 --- a/js-framework/index.ts +++ b/js-framework/index.ts @@ -16,6 +16,7 @@ export * from "./src/ui/view" export * from "./src/ui/layout" export * from "./src/ui/listview" +export * from "./src/ui/scroller" export * from "./src/ui/widgets" export * from "./src/ui/panel" export * from "./src/ui/declarative" diff --git a/js-framework/src/ui/scroller.ts b/js-framework/src/ui/scroller.ts new file mode 100644 index 00000000..88658148 --- /dev/null +++ b/js-framework/src/ui/scroller.ts @@ -0,0 +1,43 @@ +/* + * Copyright [2019] [Doric.Pub] + * + * 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. + */ +import { Superview, View, Property, IView, LayoutSpec } from './view' + +export function scroller(content: View) { + return (new Scroller).also(v => { + v.layoutConfig = { + widthSpec: LayoutSpec.WRAP_CONTENT, + heightSpec: LayoutSpec.WRAP_CONTENT, + } + v.content = content + }) +} + +export interface IScroller extends IView { + content: View +} + +export class Scroller extends Superview implements IScroller { + content!: View + + allSubviews() { + return [this.content] + } + + toModel() { + this.dirtyProps.content = this.content.viewId + return super.toModel() + } +} \ No newline at end of file