diff --git a/packages/core/ui/scroll-view/index.android.ts b/packages/core/ui/scroll-view/index.android.ts index b05166d02..17c7d06bd 100644 --- a/packages/core/ui/scroll-view/index.android.ts +++ b/packages/core/ui/scroll-view/index.android.ts @@ -9,6 +9,7 @@ export class ScrollView extends ScrollViewBase { nativeViewProtected: org.nativescript.widgets.VerticalScrollView | org.nativescript.widgets.HorizontalScrollView; private _androidViewId = -1; private handler: android.view.ViewTreeObserver.OnScrollChangedListener; + private scrollChangeHandler: androidx.core.widget.NestedScrollView.OnScrollChangeListener; get horizontalOffset(): number { const nativeView = this.nativeViewProtected; @@ -99,7 +100,13 @@ export class ScrollView extends ScrollViewBase { } public createNativeView() { - return this.orientation === 'horizontal' ? new org.nativescript.widgets.HorizontalScrollView(this._context) : new org.nativescript.widgets.VerticalScrollView(this._context); + if (this.orientation === 'horizontal') { + return new org.nativescript.widgets.HorizontalScrollView(this._context); + } else { + const view = new org.nativescript.widgets.VerticalScrollView(this._context); + view.setVerticalScrollBarEnabled(true); + return view; + } } public initNativeView(): void { @@ -123,16 +130,32 @@ export class ScrollView extends ScrollViewBase { protected attachNative() { const that = new WeakRef(this); - this.handler = new android.view.ViewTreeObserver.OnScrollChangedListener({ - onScrollChanged: function () { - const owner: ScrollView = that.get(); - if (owner) { - owner._onScrollChanged(); + if (this.orientation === 'vertical') { + this.scrollChangeHandler = new androidx.core.widget.NestedScrollView.OnScrollChangeListener({ + onScrollChange(view, scrollX, scrollY) { + const owner: ScrollView = that.get(); + if (owner) { + owner.notify({ + object: owner, + eventName: ScrollView.scrollEvent, + scrollX: layout.toDeviceIndependentPixels(scrollX), + scrollY: layout.toDeviceIndependentPixels(scrollY) + }); + } } - }, - }); - - this.nativeViewProtected.getViewTreeObserver().addOnScrollChangedListener(this.handler); + }); + this.nativeView.setOnScrollChangeListener(this.scrollChangeHandler); + } else { + this.handler = new android.view.ViewTreeObserver.OnScrollChangedListener({ + onScrollChanged: function () { + const owner: ScrollView = that.get(); + if (owner) { + owner._onScrollChanged(); + } + }, + }); + this.nativeViewProtected.getViewTreeObserver().addOnScrollChangedListener(this.handler); + } } private _lastScrollX = -1; @@ -158,8 +181,14 @@ export class ScrollView extends ScrollViewBase { } protected dettachNative() { - this.nativeViewProtected.getViewTreeObserver().removeOnScrollChangedListener(this.handler); - this.handler = null; + if (this.handler) { + this.nativeViewProtected.getViewTreeObserver().removeOnScrollChangedListener(this.handler); + this.handler = null; + } + if (this.scrollChangeHandler) { + this.nativeView.setOnScrollChangeListener(null); + this.scrollChangeHandler = null; + } } } diff --git a/packages/types-android/src/lib/android/org.nativescript.widgets.d.ts b/packages/types-android/src/lib/android/org.nativescript.widgets.d.ts index d2ea0f51b..1392ff0ad 100644 --- a/packages/types-android/src/lib/android/org.nativescript.widgets.d.ts +++ b/packages/types-android/src/lib/android/org.nativescript.widgets.d.ts @@ -359,7 +359,7 @@ constructor(context: android.content.Context); } - export class VerticalScrollView extends android.widget.ScrollView { + export class VerticalScrollView extends androidx.core.widget.NestedScrollView { constructor(context: android.content.Context); public getScrollableLength(): number; public getScrollEnabled(): boolean; diff --git a/packages/ui-mobile-base/android/widgets/src/main/java/org/nativescript/widgets/VerticalScrollView.java b/packages/ui-mobile-base/android/widgets/src/main/java/org/nativescript/widgets/VerticalScrollView.java index 23144c2d9..f59461146 100644 --- a/packages/ui-mobile-base/android/widgets/src/main/java/org/nativescript/widgets/VerticalScrollView.java +++ b/packages/ui-mobile-base/android/widgets/src/main/java/org/nativescript/widgets/VerticalScrollView.java @@ -12,70 +12,71 @@ import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.widget.FrameLayout; -import android.widget.ScrollView; +import androidx.core.widget.NestedScrollView; /** * @author hhristov * */ -public class VerticalScrollView extends ScrollView { +public class VerticalScrollView extends NestedScrollView { private final Rect mTempRect = new Rect(); - - private int contentMeasuredWidth = 0; - private int contentMeasuredHeight = 0; - private int scrollableLength = 0; - private SavedState mSavedState; - private boolean isFirstLayout = true; + + private int contentMeasuredWidth = 0; + private int contentMeasuredHeight = 0; + private int scrollableLength = 0; + private SavedState mSavedState; + private boolean isFirstLayout = true; private boolean scrollEnabled = true; - /** - * 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; - - /** - * 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 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; + + /** + * 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; + public VerticalScrollView(Context context) { super(context); } - - public int getScrollableLength() { - return this.scrollableLength; - } - public boolean getScrollEnabled() { - return this.scrollEnabled; - } + public int getScrollableLength() { + return this.scrollableLength; + } + + public boolean getScrollEnabled() { + return this.scrollEnabled; + } public void setScrollEnabled(boolean value) { this.scrollEnabled = value; } - @Override - public boolean onInterceptTouchEvent(MotionEvent ev) { - // Do nothing with intercepted touch events if we are not scrollable - if (!this.scrollEnabled) { - return false; - } + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + // Do nothing with intercepted touch events if we are not scrollable + if (!this.scrollEnabled) { + return false; + } - return super.onInterceptTouchEvent(ev); - } + return super.onInterceptTouchEvent(ev); + } - @Override - public boolean onTouchEvent(MotionEvent ev) { - if (!this.scrollEnabled && (ev.getAction() == MotionEvent.ACTION_DOWN || ev.getAction() == MotionEvent.ACTION_MOVE)) { - return false; - } + @Override + public boolean onTouchEvent(MotionEvent ev) { + if (!this.scrollEnabled + && (ev.getAction() == MotionEvent.ACTION_DOWN || ev.getAction() == MotionEvent.ACTION_MOVE)) { + return false; + } - return super.onTouchEvent(ev); - } + return super.onTouchEvent(ev); + } @Override public void requestLayout() { @@ -107,152 +108,156 @@ public class VerticalScrollView extends ScrollView { @Override protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams from) { if (from instanceof CommonLayoutParams) - return new CommonLayoutParams((CommonLayoutParams)from); + return new CommonLayoutParams((CommonLayoutParams) from); if (from instanceof FrameLayout.LayoutParams) - return new CommonLayoutParams((FrameLayout.LayoutParams)from); + return new CommonLayoutParams((FrameLayout.LayoutParams) from); if (from instanceof ViewGroup.MarginLayoutParams) - return new CommonLayoutParams((ViewGroup.MarginLayoutParams)from); + return new CommonLayoutParams((ViewGroup.MarginLayoutParams) from); return new CommonLayoutParams(from); } @Override public void requestChildFocus(View child, View focused) { - if (!this.mIsLayoutDirty) { - this.scrollToChild(focused); - } - else { - // The child may not be laid out yet, we can't compute the scroll yet - this.mChildToScrollTo = focused; - } - super.requestChildFocus(child, focused); + if (!this.mIsLayoutDirty) { + this.scrollToChild(focused); + } else { + // The child may not be laid out yet, we can't compute the scroll yet + this.mChildToScrollTo = focused; + } + super.requestChildFocus(child, focused); } - + @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - CommonLayoutParams.adjustChildrenLayoutParams(this, widthMeasureSpec, heightMeasureSpec); + CommonLayoutParams.adjustChildrenLayoutParams(this, widthMeasureSpec, heightMeasureSpec); - // Don't call measure because it will measure content twice. - // ScrollView is expected to have single child so we measure only the first child. - View child = this.getChildCount() > 0 ? this.getChildAt(0) : null; - if (child == null) { - this.scrollableLength = 0; - this.contentMeasuredWidth = 0; - this.contentMeasuredHeight = 0; - this.setPadding(0, 0, 0, 0); - } - else { - CommonLayoutParams.measureChild(child, widthMeasureSpec, MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); - this.contentMeasuredWidth = CommonLayoutParams.getDesiredWidth(child); - this.contentMeasuredHeight = CommonLayoutParams.getDesiredHeight(child); + // Don't call measure because it will measure content twice. + // ScrollView is expected to have single child so we measure only the first + // child. + View child = this.getChildCount() > 0 ? this.getChildAt(0) : null; + if (child == null) { + this.scrollableLength = 0; + this.contentMeasuredWidth = 0; + this.contentMeasuredHeight = 0; + this.setPadding(0, 0, 0, 0); + } else { + CommonLayoutParams.measureChild(child, widthMeasureSpec, + MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); + this.contentMeasuredWidth = CommonLayoutParams.getDesiredWidth(child); + this.contentMeasuredHeight = CommonLayoutParams.getDesiredHeight(child); - // Android ScrollView does not account to child margins so we set them as paddings. Otherwise you can never scroll to bottom. - CommonLayoutParams lp = (CommonLayoutParams)child.getLayoutParams(); - this.setPadding(lp.leftMargin, lp.topMargin, lp.rightMargin, lp.bottomMargin); - } - - // Don't add in our paddings because they are already added as child margins. (we will include them twice if we add them). - // check the previous line - this.setPadding(lp.leftMargin, lp.topMargin, lp.rightMargin, lp.bottomMargin); -// this.contentMeasuredWidth += this.getPaddingLeft() + this.getPaddingRight(); -// this.contentMeasuredHeight += this.getPaddingTop() + this.getPaddingBottom(); - - // Check against our minimum height - this.contentMeasuredWidth = Math.max(this.contentMeasuredWidth, this.getSuggestedMinimumWidth()); - this.contentMeasuredHeight = Math.max(this.contentMeasuredHeight, this.getSuggestedMinimumHeight()); - - int widthSizeAndState = resolveSizeAndState(this.contentMeasuredWidth, widthMeasureSpec, 0); - int heightSizeAndState = resolveSizeAndState(this.contentMeasuredHeight, heightMeasureSpec, 0); - - this.setMeasuredDimension(widthSizeAndState, heightSizeAndState); + // Android ScrollView does not account to child margins so we set them as + // paddings. Otherwise you can never scroll to bottom. + CommonLayoutParams lp = (CommonLayoutParams) child.getLayoutParams(); + this.setPadding(lp.leftMargin, lp.topMargin, lp.rightMargin, lp.bottomMargin); + } + + // Don't add in our paddings because they are already added as child margins. + // (we will include them twice if we add them). + // check the previous line - this.setPadding(lp.leftMargin, lp.topMargin, + // lp.rightMargin, lp.bottomMargin); + // this.contentMeasuredWidth += this.getPaddingLeft() + this.getPaddingRight(); + // this.contentMeasuredHeight += this.getPaddingTop() + this.getPaddingBottom(); + + // Check against our minimum height + this.contentMeasuredWidth = Math.max(this.contentMeasuredWidth, this.getSuggestedMinimumWidth()); + this.contentMeasuredHeight = Math.max(this.contentMeasuredHeight, this.getSuggestedMinimumHeight()); + + int widthSizeAndState = resolveSizeAndState(this.contentMeasuredWidth, widthMeasureSpec, 0); + int heightSizeAndState = resolveSizeAndState(this.contentMeasuredHeight, heightMeasureSpec, 0); + + this.setMeasuredDimension(widthSizeAndState, heightSizeAndState); } - + @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { int childHeight = 0; if (this.getChildCount() > 0) { - View child = this.getChildAt(0); - childHeight = child.getMeasuredHeight(); - - int width = right - left; - int height = bottom - top; - - this.scrollableLength = this.contentMeasuredHeight - height; - CommonLayoutParams.layoutChild(child, 0, 0, width, Math.max(this.contentMeasuredHeight, height)); - this.scrollableLength = Math.max(0, this.scrollableLength); + View child = this.getChildAt(0); + childHeight = child.getMeasuredHeight(); + + int width = right - left; + int height = bottom - top; + + this.scrollableLength = this.contentMeasuredHeight - height; + CommonLayoutParams.layoutChild(child, 0, 0, width, Math.max(this.contentMeasuredHeight, height)); + this.scrollableLength = Math.max(0, this.scrollableLength); } - + this.mIsLayoutDirty = false; - // Give a child focus if it needs it - if (this.mChildToScrollTo != null && HorizontalScrollView.isViewDescendantOf(this.mChildToScrollTo, this)) { - this.scrollToChild(this.mChildToScrollTo); - } - - this.mChildToScrollTo = null; - - int scrollX = this.getScrollX(); - int scrollY = this.getScrollY(); - if (this.isFirstLayout) { - this.isFirstLayout = false; - - final int scrollRange = Math.max(0, childHeight - (bottom - top - this.getPaddingTop() - this.getPaddingBottom())); - if (this.mSavedState != null) { - scrollY = mSavedState.scrollPosition; - mSavedState = null; - } - - // Don't forget to clamp - if (scrollY > scrollRange) { - scrollY = scrollRange; - } else if (scrollY < 0) { - scrollY = 0; - } - } - - // Calling this with the present values causes it to re-claim them - this.scrollTo(scrollX, scrollY); + // Give a child focus if it needs it + if (this.mChildToScrollTo != null && HorizontalScrollView.isViewDescendantOf(this.mChildToScrollTo, this)) { + this.scrollToChild(this.mChildToScrollTo); + } + + this.mChildToScrollTo = null; + + int scrollX = this.getScrollX(); + int scrollY = this.getScrollY(); + if (this.isFirstLayout) { + this.isFirstLayout = false; + + final int scrollRange = Math.max(0, + childHeight - (bottom - top - this.getPaddingTop() - this.getPaddingBottom())); + if (this.mSavedState != null) { + scrollY = mSavedState.scrollPosition; + mSavedState = null; + } + + // Don't forget to clamp + if (scrollY > scrollRange) { + scrollY = scrollRange; + } else if (scrollY < 0) { + scrollY = 0; + } + } + + // Calling this with the present values causes it to re-claim them + this.scrollTo(scrollX, scrollY); CommonLayoutParams.restoreOriginalParams(this); } - @Override - protected void onAttachedToWindow() { - super.onAttachedToWindow(); - this.isFirstLayout = true; - } - - @Override - protected void onDetachedFromWindow() { - super.onDetachedFromWindow(); - this.isFirstLayout = true; - } - + @Override + public void onAttachedToWindow() { + super.onAttachedToWindow(); + this.isFirstLayout = true; + } + + @Override + public void onDetachedFromWindow() { + super.onDetachedFromWindow(); + this.isFirstLayout = true; + } + @Override protected void onRestoreInstanceState(Parcelable state) { - SavedState ss = (SavedState) state; - super.onRestoreInstanceState(ss.getSuperState()); - this.mSavedState = ss; - this.requestLayout(); + SavedState ss = (SavedState) state; + super.onRestoreInstanceState(ss.getSuperState()); + this.mSavedState = ss; + this.requestLayout(); } - + @Override protected Parcelable onSaveInstanceState() { - Parcelable superState = super.onSaveInstanceState(); - SavedState ss = new SavedState(superState); - ss.scrollPosition = this.getScrollY(); - return ss; + Parcelable superState = super.onSaveInstanceState(); + SavedState ss = new SavedState(superState); + ss.scrollPosition = this.getScrollY(); + return ss; } - + 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) { - this.scrollBy(scrollDelta, 0); - } + child.getDrawingRect(mTempRect); + + /* Offset from child's local coordinates to ScrollView coordinates */ + offsetDescendantRectToMyCoords(child, mTempRect); + + int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect); + if (scrollDelta != 0) { + this.scrollBy(scrollDelta, 0); + } } } \ No newline at end of file