Merge branch 'nestedscrollview' of github.com:Akylas/NativeScript

This commit is contained in:
Martin Guillon
2021-02-08 10:09:24 +01:00
3 changed files with 204 additions and 170 deletions

View File

@ -9,6 +9,7 @@ export class ScrollView extends ScrollViewBase {
nativeViewProtected: org.nativescript.widgets.VerticalScrollView | org.nativescript.widgets.HorizontalScrollView; nativeViewProtected: org.nativescript.widgets.VerticalScrollView | org.nativescript.widgets.HorizontalScrollView;
private _androidViewId = -1; private _androidViewId = -1;
private handler: android.view.ViewTreeObserver.OnScrollChangedListener; private handler: android.view.ViewTreeObserver.OnScrollChangedListener;
private scrollChangeHandler: androidx.core.widget.NestedScrollView.OnScrollChangeListener;
get horizontalOffset(): number { get horizontalOffset(): number {
const nativeView = this.nativeViewProtected; const nativeView = this.nativeViewProtected;
@ -99,7 +100,13 @@ export class ScrollView extends ScrollViewBase {
} }
public createNativeView() { 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 { public initNativeView(): void {
@ -123,16 +130,32 @@ export class ScrollView extends ScrollViewBase {
protected attachNative() { protected attachNative() {
const that = new WeakRef(this); const that = new WeakRef(this);
this.handler = new android.view.ViewTreeObserver.OnScrollChangedListener({ if (this.orientation === 'vertical') {
onScrollChanged: function () { this.scrollChangeHandler = new androidx.core.widget.NestedScrollView.OnScrollChangeListener({
const owner: ScrollView = that.get(); onScrollChange(view, scrollX, scrollY) {
if (owner) { const owner: ScrollView = that.get();
owner._onScrollChanged(); if (owner) {
owner.notify({
object: owner,
eventName: ScrollView.scrollEvent,
scrollX: layout.toDeviceIndependentPixels(scrollX),
scrollY: layout.toDeviceIndependentPixels(scrollY)
});
}
} }
}, });
}); this.nativeView.setOnScrollChangeListener(this.scrollChangeHandler);
} else {
this.nativeViewProtected.getViewTreeObserver().addOnScrollChangedListener(this.handler); 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; private _lastScrollX = -1;
@ -158,8 +181,14 @@ export class ScrollView extends ScrollViewBase {
} }
protected dettachNative() { protected dettachNative() {
this.nativeViewProtected.getViewTreeObserver().removeOnScrollChangedListener(this.handler); if (this.handler) {
this.handler = null; this.nativeViewProtected.getViewTreeObserver().removeOnScrollChangedListener(this.handler);
this.handler = null;
}
if (this.scrollChangeHandler) {
this.nativeView.setOnScrollChangeListener(null);
this.scrollChangeHandler = null;
}
} }
} }

View File

@ -359,7 +359,7 @@
constructor(context: android.content.Context); 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); constructor(context: android.content.Context);
public getScrollableLength(): number; public getScrollableLength(): number;
public getScrollEnabled(): boolean; public getScrollEnabled(): boolean;

View File

@ -12,70 +12,71 @@ import android.view.MotionEvent;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.FrameLayout; import android.widget.FrameLayout;
import android.widget.ScrollView; import androidx.core.widget.NestedScrollView;
/** /**
* @author hhristov * @author hhristov
* *
*/ */
public class VerticalScrollView extends ScrollView { public class VerticalScrollView extends NestedScrollView {
private final Rect mTempRect = new Rect(); private final Rect mTempRect = new Rect();
private int contentMeasuredWidth = 0; private int contentMeasuredWidth = 0;
private int contentMeasuredHeight = 0; private int contentMeasuredHeight = 0;
private int scrollableLength = 0; private int scrollableLength = 0;
private SavedState mSavedState; private SavedState mSavedState;
private boolean isFirstLayout = true; private boolean isFirstLayout = true;
private boolean scrollEnabled = true; private boolean scrollEnabled = true;
/** /**
* True when the layout has changed but the traversal has not come through yet. * 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. * Ideally the view hierarchy would keep track of this for us.
*/ */
private boolean mIsLayoutDirty = true; private boolean mIsLayoutDirty = true;
/** /**
* The child to give focus to in the event that a child has requested focus while the * The child to give focus to in the event that a child has requested focus
* layout is dirty. This prevents the scroll from being wrong if the child has not been * while the layout is dirty. This prevents the scroll from being wrong if the
* laid out before requesting focus. * child has not been laid out before requesting focus.
*/ */
private View mChildToScrollTo = null; private View mChildToScrollTo = null;
public VerticalScrollView(Context context) { public VerticalScrollView(Context context) {
super(context); super(context);
} }
public int getScrollableLength() {
return this.scrollableLength;
}
public boolean getScrollEnabled() { public int getScrollableLength() {
return this.scrollEnabled; return this.scrollableLength;
} }
public boolean getScrollEnabled() {
return this.scrollEnabled;
}
public void setScrollEnabled(boolean value) { public void setScrollEnabled(boolean value) {
this.scrollEnabled = value; this.scrollEnabled = value;
} }
@Override @Override
public boolean onInterceptTouchEvent(MotionEvent ev) { public boolean onInterceptTouchEvent(MotionEvent ev) {
// Do nothing with intercepted touch events if we are not scrollable // Do nothing with intercepted touch events if we are not scrollable
if (!this.scrollEnabled) { if (!this.scrollEnabled) {
return false; return false;
} }
return super.onInterceptTouchEvent(ev); return super.onInterceptTouchEvent(ev);
} }
@Override @Override
public boolean onTouchEvent(MotionEvent ev) { public boolean onTouchEvent(MotionEvent ev) {
if (!this.scrollEnabled && (ev.getAction() == MotionEvent.ACTION_DOWN || ev.getAction() == MotionEvent.ACTION_MOVE)) { if (!this.scrollEnabled
return false; && (ev.getAction() == MotionEvent.ACTION_DOWN || ev.getAction() == MotionEvent.ACTION_MOVE)) {
} return false;
}
return super.onTouchEvent(ev); return super.onTouchEvent(ev);
} }
@Override @Override
public void requestLayout() { public void requestLayout() {
@ -107,152 +108,156 @@ public class VerticalScrollView extends ScrollView {
@Override @Override
protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams from) { protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams from) {
if (from instanceof CommonLayoutParams) if (from instanceof CommonLayoutParams)
return new CommonLayoutParams((CommonLayoutParams)from); return new CommonLayoutParams((CommonLayoutParams) from);
if (from instanceof FrameLayout.LayoutParams) if (from instanceof FrameLayout.LayoutParams)
return new CommonLayoutParams((FrameLayout.LayoutParams)from); return new CommonLayoutParams((FrameLayout.LayoutParams) from);
if (from instanceof ViewGroup.MarginLayoutParams) if (from instanceof ViewGroup.MarginLayoutParams)
return new CommonLayoutParams((ViewGroup.MarginLayoutParams)from); return new CommonLayoutParams((ViewGroup.MarginLayoutParams) from);
return new CommonLayoutParams(from); return new CommonLayoutParams(from);
} }
@Override @Override
public void requestChildFocus(View child, View focused) { public void requestChildFocus(View child, View focused) {
if (!this.mIsLayoutDirty) { if (!this.mIsLayoutDirty) {
this.scrollToChild(focused); this.scrollToChild(focused);
} } else {
else { // The child may not be laid out yet, we can't compute the scroll yet
// The child may not be laid out yet, we can't compute the scroll yet this.mChildToScrollTo = focused;
this.mChildToScrollTo = focused; }
} super.requestChildFocus(child, focused);
super.requestChildFocus(child, focused);
} }
@Override @Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 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. // Don't call measure because it will measure content twice.
// ScrollView is expected to have single child so we measure only the first child. // ScrollView is expected to have single child so we measure only the first
View child = this.getChildCount() > 0 ? this.getChildAt(0) : null; // child.
if (child == null) { View child = this.getChildCount() > 0 ? this.getChildAt(0) : null;
this.scrollableLength = 0; if (child == null) {
this.contentMeasuredWidth = 0; this.scrollableLength = 0;
this.contentMeasuredHeight = 0; this.contentMeasuredWidth = 0;
this.setPadding(0, 0, 0, 0); this.contentMeasuredHeight = 0;
} this.setPadding(0, 0, 0, 0);
else { } else {
CommonLayoutParams.measureChild(child, widthMeasureSpec, MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); CommonLayoutParams.measureChild(child, widthMeasureSpec,
this.contentMeasuredWidth = CommonLayoutParams.getDesiredWidth(child); MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
this.contentMeasuredHeight = CommonLayoutParams.getDesiredHeight(child); 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. // Android ScrollView does not account to child margins so we set them as
CommonLayoutParams lp = (CommonLayoutParams)child.getLayoutParams(); // paddings. Otherwise you can never scroll to bottom.
this.setPadding(lp.leftMargin, lp.topMargin, lp.rightMargin, lp.bottomMargin); 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); // Don't add in our paddings because they are already added as child margins.
// this.contentMeasuredWidth += this.getPaddingLeft() + this.getPaddingRight(); // (we will include them twice if we add them).
// this.contentMeasuredHeight += this.getPaddingTop() + this.getPaddingBottom(); // check the previous line - this.setPadding(lp.leftMargin, lp.topMargin,
// lp.rightMargin, lp.bottomMargin);
// Check against our minimum height // this.contentMeasuredWidth += this.getPaddingLeft() + this.getPaddingRight();
this.contentMeasuredWidth = Math.max(this.contentMeasuredWidth, this.getSuggestedMinimumWidth()); // this.contentMeasuredHeight += this.getPaddingTop() + this.getPaddingBottom();
this.contentMeasuredHeight = Math.max(this.contentMeasuredHeight, this.getSuggestedMinimumHeight());
// Check against our minimum height
int widthSizeAndState = resolveSizeAndState(this.contentMeasuredWidth, widthMeasureSpec, 0); this.contentMeasuredWidth = Math.max(this.contentMeasuredWidth, this.getSuggestedMinimumWidth());
int heightSizeAndState = resolveSizeAndState(this.contentMeasuredHeight, heightMeasureSpec, 0); this.contentMeasuredHeight = Math.max(this.contentMeasuredHeight, this.getSuggestedMinimumHeight());
this.setMeasuredDimension(widthSizeAndState, heightSizeAndState); int widthSizeAndState = resolveSizeAndState(this.contentMeasuredWidth, widthMeasureSpec, 0);
int heightSizeAndState = resolveSizeAndState(this.contentMeasuredHeight, heightMeasureSpec, 0);
this.setMeasuredDimension(widthSizeAndState, heightSizeAndState);
} }
@Override @Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) { protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
int childHeight = 0; int childHeight = 0;
if (this.getChildCount() > 0) { if (this.getChildCount() > 0) {
View child = this.getChildAt(0); View child = this.getChildAt(0);
childHeight = child.getMeasuredHeight(); childHeight = child.getMeasuredHeight();
int width = right - left; int width = right - left;
int height = bottom - top; int height = bottom - top;
this.scrollableLength = this.contentMeasuredHeight - height; this.scrollableLength = this.contentMeasuredHeight - height;
CommonLayoutParams.layoutChild(child, 0, 0, width, Math.max(this.contentMeasuredHeight, height)); CommonLayoutParams.layoutChild(child, 0, 0, width, Math.max(this.contentMeasuredHeight, height));
this.scrollableLength = Math.max(0, this.scrollableLength); this.scrollableLength = Math.max(0, this.scrollableLength);
} }
this.mIsLayoutDirty = false; this.mIsLayoutDirty = false;
// Give a child focus if it needs it // Give a child focus if it needs it
if (this.mChildToScrollTo != null && HorizontalScrollView.isViewDescendantOf(this.mChildToScrollTo, this)) { if (this.mChildToScrollTo != null && HorizontalScrollView.isViewDescendantOf(this.mChildToScrollTo, this)) {
this.scrollToChild(this.mChildToScrollTo); this.scrollToChild(this.mChildToScrollTo);
} }
this.mChildToScrollTo = null; this.mChildToScrollTo = null;
int scrollX = this.getScrollX(); int scrollX = this.getScrollX();
int scrollY = this.getScrollY(); int scrollY = this.getScrollY();
if (this.isFirstLayout) { if (this.isFirstLayout) {
this.isFirstLayout = false; this.isFirstLayout = false;
final int scrollRange = Math.max(0, childHeight - (bottom - top - this.getPaddingTop() - this.getPaddingBottom())); final int scrollRange = Math.max(0,
if (this.mSavedState != null) { childHeight - (bottom - top - this.getPaddingTop() - this.getPaddingBottom()));
scrollY = mSavedState.scrollPosition; if (this.mSavedState != null) {
mSavedState = null; scrollY = mSavedState.scrollPosition;
} mSavedState = null;
}
// Don't forget to clamp
if (scrollY > scrollRange) { // Don't forget to clamp
scrollY = scrollRange; if (scrollY > scrollRange) {
} else if (scrollY < 0) { scrollY = scrollRange;
scrollY = 0; } else if (scrollY < 0) {
} scrollY = 0;
} }
}
// Calling this with the present values causes it to re-claim them
this.scrollTo(scrollX, scrollY); // Calling this with the present values causes it to re-claim them
this.scrollTo(scrollX, scrollY);
CommonLayoutParams.restoreOriginalParams(this); CommonLayoutParams.restoreOriginalParams(this);
} }
@Override @Override
protected void onAttachedToWindow() { public void onAttachedToWindow() {
super.onAttachedToWindow(); super.onAttachedToWindow();
this.isFirstLayout = true; this.isFirstLayout = true;
} }
@Override @Override
protected void onDetachedFromWindow() { public void onDetachedFromWindow() {
super.onDetachedFromWindow(); super.onDetachedFromWindow();
this.isFirstLayout = true; this.isFirstLayout = true;
} }
@Override @Override
protected void onRestoreInstanceState(Parcelable state) { protected void onRestoreInstanceState(Parcelable state) {
SavedState ss = (SavedState) state; SavedState ss = (SavedState) state;
super.onRestoreInstanceState(ss.getSuperState()); super.onRestoreInstanceState(ss.getSuperState());
this.mSavedState = ss; this.mSavedState = ss;
this.requestLayout(); this.requestLayout();
} }
@Override @Override
protected Parcelable onSaveInstanceState() { protected Parcelable onSaveInstanceState() {
Parcelable superState = super.onSaveInstanceState(); Parcelable superState = super.onSaveInstanceState();
SavedState ss = new SavedState(superState); SavedState ss = new SavedState(superState);
ss.scrollPosition = this.getScrollY(); ss.scrollPosition = this.getScrollY();
return ss; return ss;
} }
private void scrollToChild(View child) { private void scrollToChild(View child) {
child.getDrawingRect(mTempRect); child.getDrawingRect(mTempRect);
/* Offset from child's local coordinates to ScrollView coordinates */ /* Offset from child's local coordinates to ScrollView coordinates */
offsetDescendantRectToMyCoords(child, mTempRect); offsetDescendantRectToMyCoords(child, mTempRect);
int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect); int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect);
if (scrollDelta != 0) { if (scrollDelta != 0) {
this.scrollBy(scrollDelta, 0); this.scrollBy(scrollDelta, 0);
} }
} }
} }