package com.gh.common.view; import android.content.Context; import android.os.Bundle; import android.util.AttributeSet; import android.util.Log; import android.util.TypedValue; import android.view.MotionEvent; import android.view.VelocityTracker; import android.view.View; import android.view.ViewConfiguration; import android.view.ViewParent; import android.view.accessibility.AccessibilityEvent; import android.view.animation.AnimationUtils; import android.webkit.WebView; import android.widget.OverScroller; import android.widget.ScrollView; 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.ViewCompat; import androidx.core.view.accessibility.AccessibilityNodeInfoCompat; import androidx.core.view.accessibility.AccessibilityRecordCompat; import wendu.dsbridge.DWebView; /** * Copyright (c) Tuenti Technologies. All rights reserved. *

* WebView compatible with CoordinatorLayout. * The implementation based on NestedScrollView of design library */ public class NestedScrollWebView2 extends DWebView implements NestedScrollingChild2, NestedScrollingParent { private static final int INVALID_POINTER = -1; private static final String TAG = "NestedWebView"; private final int[] mScrollOffset = new int[2]; private final int[] mScrollConsumed = new int[2]; private int mLastMotionY; private NestedScrollingParentHelper mParentHelper; private NestedScrollingChildHelper mChildHelper; private boolean mIsBeingDragged = false; private VelocityTracker mVelocityTracker; private int mTouchSlop; private int mActivePointerId = INVALID_POINTER; private int mNestedYOffset; private OverScroller mScroller; private int mMinimumVelocity; private int mMaximumVelocity; private static final AccessibilityDelegate ACCESSIBILITY_DELEGATE = new AccessibilityDelegate(); public NestedScrollWebView2(Context context, AttributeSet attrs) { super(context, attrs); init(); } public NestedScrollWebView2(Context context) { super(context); init(); } private void init(){ setOverScrollMode(WebView.OVER_SCROLL_NEVER); initScrollView(); mChildHelper = new NestedScrollingChildHelper(this); mParentHelper = new NestedScrollingParentHelper(this); setNestedScrollingEnabled(true); ViewCompat.setAccessibilityDelegate(this, ACCESSIBILITY_DELEGATE); } private void initScrollView() { mScroller = new OverScroller(getContext()); final ViewConfiguration configuration = ViewConfiguration.get(getContext()); mTouchSlop = configuration.getScaledTouchSlop(); mMinimumVelocity = configuration.getScaledMinimumFlingVelocity(); mMaximumVelocity = configuration.getScaledMaximumFlingVelocity(); } @Override public boolean dispatchTouchEvent(MotionEvent ev) { getParent().requestDisallowInterceptTouchEvent(true); return super.dispatchTouchEvent(ev); } @Override public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) { if (disallowIntercept) { recycleVelocityTracker(); } super.requestDisallowInterceptTouchEvent(disallowIntercept); } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { final int action = ev.getAction(); if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) { return true; } switch (action & MotionEvent.ACTION_MASK) { case MotionEvent.ACTION_MOVE: { final int activePointerId = mActivePointerId; if (activePointerId == INVALID_POINTER) { break; } final int pointerIndex = ev.findPointerIndex(activePointerId); if (pointerIndex == -1) { Log.e(TAG, "Invalid pointerId=" + activePointerId + " in onInterceptTouchEvent"); break; } final int y = (int) ev.getY(pointerIndex); final int yDiff = Math.abs(y - mLastMotionY); if (yDiff > mTouchSlop && (getNestedScrollAxes() & ViewCompat.SCROLL_AXIS_VERTICAL) == 0) { mIsBeingDragged = true; mLastMotionY = y; initVelocityTrackerIfNotExists(); mVelocityTracker.addMovement(ev); mNestedYOffset = 0; final ViewParent parent = getParent(); if (parent != null) { parent.requestDisallowInterceptTouchEvent(true); } } break; } case MotionEvent.ACTION_DOWN: { final int y = (int) ev.getY(); mLastMotionY = y; mActivePointerId = ev.getPointerId(0); initOrResetVelocityTracker(); mVelocityTracker.addMovement(ev); mScroller.computeScrollOffset(); mIsBeingDragged = !mScroller.isFinished(); startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL); break; } case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_UP: mIsBeingDragged = false; mActivePointerId = INVALID_POINTER; recycleVelocityTracker(); if (mScroller.springBack(getScrollX(), getScrollY(), 0, 0, 0, getScrollRange())) { ViewCompat.postInvalidateOnAnimation(this); } stopNestedScroll(); break; case MotionEvent.ACTION_POINTER_UP: onSecondaryPointerUp(ev); break; } return mIsBeingDragged; } @Override public boolean onTouchEvent(MotionEvent ev) { boolean returnValue = false; initVelocityTrackerIfNotExists(); MotionEvent vtev = MotionEvent.obtain(ev); final int actionMasked = ev.getActionMasked(); if (actionMasked == MotionEvent.ACTION_DOWN) { mNestedYOffset = 0; } vtev.offsetLocation(0, mNestedYOffset); switch (actionMasked) { case MotionEvent.ACTION_DOWN: { returnValue = super.onTouchEvent(ev); if (mIsBeingDragged = !mScroller.isFinished()) { final ViewParent parent = getParent(); if (parent != null) { parent.requestDisallowInterceptTouchEvent(true); } } if (!mScroller.isFinished()) { mScroller.abortAnimation(); } mLastMotionY = (int) ev.getY(); mActivePointerId = ev.getPointerId(0); startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, 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 y = (int) ev.getY(activePointerIndex); int deltaY = mLastMotionY - y; if (dispatchNestedPreScroll(0, deltaY, mScrollConsumed, mScrollOffset, ViewCompat.TYPE_TOUCH)) { deltaY -= mScrollConsumed[1]; ev.offsetLocation(0, -mScrollOffset[1]); vtev.offsetLocation(0, -mScrollOffset[1]); mNestedYOffset += mScrollOffset[1]; } boolean notMove = mScrollOffset[1] == 0; if (!mIsBeingDragged && Math.abs(deltaY) > mTouchSlop) { final ViewParent parent = getParent(); if (parent != null) { parent.requestDisallowInterceptTouchEvent(true); } mIsBeingDragged = true; if (deltaY > 0) { deltaY -= mTouchSlop; } else { deltaY += mTouchSlop; } } if (mIsBeingDragged) { mLastMotionY = y - mScrollOffset[1]; final int oldY = getScrollY(); final int range = getScrollRange(); int unconsumedY = 0; int scrolledDeltaY = deltaY; int expectScroll = oldY + deltaY; if (expectScroll < 0) { unconsumedY = expectScroll; scrolledDeltaY = oldY; } else if (expectScroll > range) { unconsumedY = range - expectScroll; scrolledDeltaY = expectScroll - range; } if (dispatchNestedScroll(0, scrolledDeltaY, 0, unconsumedY, mScrollOffset, ViewCompat.TYPE_TOUCH)) { vtev.offsetLocation(0, mScrollOffset[1]); mNestedYOffset += mScrollOffset[1]; mLastMotionY -= mScrollOffset[1]; } } notMove &= (mScrollOffset[1] == 0); if (notMove) { returnValue = super.onTouchEvent(ev); } else { final ViewParent parent = getParent(); if (parent != null) { parent.requestDisallowInterceptTouchEvent(true); } } break; case MotionEvent.ACTION_UP: if (Math.abs(mNestedYOffset) < mTouchSlop) { returnValue = super.onTouchEvent(ev); } else { ev.setAction(MotionEvent.ACTION_CANCEL); returnValue = super.onTouchEvent(ev); } final VelocityTracker velocityTracker = mVelocityTracker; velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); int initialVelocity = (int) velocityTracker.getYVelocity(mActivePointerId); if ((Math.abs(initialVelocity) > mMinimumVelocity)) { flingWithNestedDispatch(-initialVelocity); } else if (mScroller.springBack(getScrollX(), getScrollY(), 0, 0, 0, getScrollRange())) { ViewCompat.postInvalidateOnAnimation(this); } mActivePointerId = INVALID_POINTER; endDrag(); break; case MotionEvent.ACTION_CANCEL: returnValue = true; if (mIsBeingDragged && getChildCount() > 0) { if (mScroller.springBack(getScrollX(), getScrollY(), 0, 0, 0, getScrollRange())) { ViewCompat.postInvalidateOnAnimation(this); } } mActivePointerId = INVALID_POINTER; endDrag(); break; case MotionEvent.ACTION_POINTER_DOWN: { final int index = ev.getActionIndex(); mLastMotionY = (int) ev.getY(index); mActivePointerId = ev.getPointerId(index); break; } case MotionEvent.ACTION_POINTER_UP: onSecondaryPointerUp(ev); mLastMotionY = (int) ev.getY(ev.findPointerIndex(mActivePointerId)); break; } if (mVelocityTracker != null) { mVelocityTracker.addMovement(vtev); } vtev.recycle(); return returnValue; } int getScrollRange() { //Using scroll range of webview instead of childs as NestedScrollView does. return computeVerticalScrollRange(); } @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 (clampedY && !hasNestedScrollingParent(ViewCompat.TYPE_NON_TOUCH)) { mScroller.springBack(newScrollX, newScrollY, 0, 0, 0, getScrollRange()); } onOverScrolled(newScrollX, newScrollY, clampedX, clampedY); return clampedX || clampedY; } private float mVerticalScrollFactor; @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 vscroll = event.getAxisValue(MotionEvent.AXIS_VSCROLL); if (vscroll != 0) { final int delta = (int) (vscroll * getVerticalScrollFactorCompat()); final int range = getScrollRange(); int oldScrollY = getScrollY(); int newScrollY = oldScrollY - delta; if (newScrollY < 0) { newScrollY = 0; } else if (newScrollY > range) { newScrollY = range; } if (newScrollY != oldScrollY) { super.scrollTo(getScrollX(), newScrollY); return true; } } } } } } return false; } 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; } private void endDrag() { mIsBeingDragged = false; recycleVelocityTracker(); stopNestedScroll(); } 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); mActivePointerId = ev.getPointerId(newPointerIndex); if (mVelocityTracker != null) { mVelocityTracker.clear(); } } } 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; } } private void flingWithNestedDispatch(int velocityY) { final int scrollY = getScrollY(); final boolean canFling = (scrollY > 0 || velocityY > 0) && (scrollY < getScrollRange() || velocityY < 0); if (!dispatchNestedPreFling(0, velocityY)) { dispatchNestedFling(0, velocityY, canFling); fling(velocityY); } } public void fling(int velocityY) { startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_NON_TOUCH); mScroller.fling(getScrollX(), getScrollY(), // start 0, velocityY, // velocities 0, 0, // x Integer.MIN_VALUE, Integer.MAX_VALUE, // y 0, 0); // overscroll mLastScrollerY = getScrollY(); ViewCompat.postInvalidateOnAnimation(this); } private int mLastScrollerY; @Override public void computeScroll() { if (mScroller.computeScrollOffset()) { final int x = mScroller.getCurrX(); final int y = mScroller.getCurrY(); int dy = y - mLastScrollerY; // Dispatch up to parent if (dispatchNestedPreScroll(0, dy, mScrollConsumed, null, ViewCompat.TYPE_NON_TOUCH)) { dy -= mScrollConsumed[1]; } if (dy != 0) { final int range = getScrollRange(); final int oldScrollY = getScrollY(); overScrollByCompat(0, dy, getScrollX(), oldScrollY, 0, range, 0, 0, false); final int scrolledDeltaY = getScrollY() - oldScrollY; final int unconsumedY = dy - scrolledDeltaY; if (!dispatchNestedScroll(0, scrolledDeltaY, 0, unconsumedY, null, ViewCompat.TYPE_NON_TOUCH)) { final int mode = getOverScrollMode(); final boolean canOverscroll = mode == OVER_SCROLL_ALWAYS || (mode == OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0); if (canOverscroll) { // ensureGlows(); // if (y <= 0 && oldScrollY > 0) { // mEdgeGlowTop.onAbsorb((int) mScroller.getCurrVelocity()); // } else if (y >= range && oldScrollY < range) { // mEdgeGlowBottom.onAbsorb((int) mScroller.getCurrVelocity()); // } } } } // Finally update the scroll positions and post an invalidation 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 mLastScrollerY = 0; } } @Override public boolean isNestedScrollingEnabled() { return mChildHelper.isNestedScrollingEnabled(); } @Override public void setNestedScrollingEnabled(boolean enabled) { mChildHelper.setNestedScrollingEnabled(enabled); } @Override public boolean startNestedScroll(int axes) { return mChildHelper.startNestedScroll(axes); } @Override public void stopNestedScroll() { mChildHelper.stopNestedScroll(); } @Override public boolean hasNestedScrollingParent() { return mChildHelper.hasNestedScrollingParent(); } @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 dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) { return mChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow); } @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); } @Override public int getNestedScrollAxes() { return mParentHelper.getNestedScrollAxes(); } @Override public boolean startNestedScroll(int axes, int type) { return mChildHelper.startNestedScroll(axes, type); } @Override public void stopNestedScroll(int type) { mChildHelper.stopNestedScroll(type); } @Override public boolean hasNestedScrollingParent(int type) { return mChildHelper.hasNestedScrollingParent(type); } @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, int type) { return mChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow, type); } @Override public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) { final int oldScrollY = getScrollY(); scrollBy(0, dyUnconsumed); final int myConsumed = getScrollY() - oldScrollY; final int myUnconsumed = dyUnconsumed - myConsumed; dispatchNestedScroll(0, myConsumed, 0, myUnconsumed, 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) velocityY); return true; } return false; } @Override public boolean onNestedPreFling(View target, float velocityX, float velocityY) { return dispatchNestedPreFling(velocityX, velocityY); } // nested scroll parent @Override public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) { return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0; } @Override public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes) { mParentHelper.onNestedScrollAccepted(child, target, nestedScrollAxes); startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL); } @Override public void onStopNestedScroll(View target) { mParentHelper.onStopNestedScroll(target); stopNestedScroll(); } /** * 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()); } /** * 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 */ private long mLastScroll; static final int ANIMATED_SCROLL_GAP = 250; public final void smoothScrollBy(int dx, int dy) { long duration = AnimationUtils.currentAnimationTimeMillis() - mLastScroll; if (duration > ANIMATED_SCROLL_GAP) { final int height = getHeight() - getPaddingBottom() - getPaddingTop(); final int bottom = getHeight(); final int maxY = Math.max(0, bottom - height); final int scrollY = getScrollY(); dy = Math.max(0, Math.min(scrollY + dy, maxY)) - scrollY; mScroller.startScroll(getScrollX(), scrollY, 0, dy); ViewCompat.postInvalidateOnAnimation(this); } else { if (!mScroller.isFinished()) { mScroller.abortAnimation(); } scrollBy(dx, dy); } mLastScroll = AnimationUtils.currentAnimationTimeMillis(); } static class AccessibilityDelegate extends AccessibilityDelegateCompat { @Override public boolean performAccessibilityAction(View host, int action, Bundle arguments) { if (super.performAccessibilityAction(host, action, arguments)) { return true; } final NestedScrollWebView2 nsvHost = (NestedScrollWebView2) 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.getScrollRange()); 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 NestedScrollWebView2 nsvHost = (NestedScrollWebView2) host; info.setClassName(ScrollView.class.getName()); if (nsvHost.isEnabled()) { final int scrollRange = nsvHost.getScrollRange(); 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 NestedScrollWebView2 nsvHost = (NestedScrollWebView2) host; event.setClassName(ScrollView.class.getName()); final boolean scrollable = nsvHost.getScrollRange() > 0; event.setScrollable(scrollable); event.setScrollX(nsvHost.getScrollX()); event.setScrollY(nsvHost.getScrollY()); AccessibilityRecordCompat.setMaxScrollX(event, nsvHost.getScrollX()); AccessibilityRecordCompat.setMaxScrollY(event, nsvHost.getScrollRange()); } } /** * Stop any current scroll * #fling(int, int)} or a touch-initiated fling. */ public void stopScroll() { if (mScroller != null) { mScroller.forceFinished(true); } } }