حرکت حرکتی اسکرول را متحرک کنید

روش Compose را امتحان کنید
Jetpack Compose جعبه ابزار UI توصیه شده برای اندروید است. با نحوه استفاده از لمس و ورودی در Compose آشنا شوید.

در اندروید، اسکرول معمولاً با استفاده از کلاس ScrollView انجام می شود. هر طرح‌بندی استانداردی را که ممکن است فراتر از محدوده محفظه آن گسترش یابد، در یک ScrollView قرار دهید تا نمای قابل پیمایشی را ارائه دهد که توسط چارچوب مدیریت می‌شود. پیاده سازی یک اسکرول سفارشی فقط برای سناریوهای خاص ضروری است. این سند نحوه نمایش افکت اسکرول را در پاسخ به حرکات لمسی با استفاده از اسکرول توضیح می دهد.

برنامه شما می‌تواند از پیمایش‌ها - Scroller یا OverScroller - برای جمع‌آوری داده‌های مورد نیاز برای تولید یک انیمیشن پیمایشی در پاسخ به یک رویداد لمسی استفاده کند. آنها مشابه هستند، اما OverScroller همچنین شامل روش هایی برای نشان دادن زمانی است که کاربران پس از حرکت حرکت یا حرکت به لبه های محتوا می رسند.

  • با شروع در Android 12 (سطح API 31)، عناصر بصری در یک رویداد درگ کشیده می‌شوند و به عقب باز می‌گردند و در یک رویداد پرتاب به عقب باز می‌گردند.
  • در اندروید 11 (سطح API 30) و قبل از آن، مرزها یک جلوه "درخشش" را پس از حرکت کشیدن یا پرت کردن به لبه نشان می دهند.

نمونه InteractiveChart در این سند از کلاس EdgeEffect برای نمایش این افکت های overscroll استفاده می کند.

می‌توانید با استفاده از فیزیک اسکرول استاندارد پلت‌فرم مانند اصطکاک، سرعت و کیفیت‌های دیگر، از پیمایش برای متحرک کردن پیمایش در طول زمان استفاده کنید. خود اسکرول چیزی نمی کشد. اسکرول‌ها در طول زمان انحراف‌های پیمایش را برای شما دنبال می‌کنند، اما به‌طور خودکار این موقعیت‌ها را در نمای شما اعمال نمی‌کنند. شما باید مختصات جدید را با سرعتی دریافت و اعمال کنید که انیمیشن اسکرول را صاف به نظر برساند.

اصطلاحات اسکرول را درک کنید

اسکرول کلمه ای است که بسته به زمینه آن در اندروید می تواند معانی مختلفی داشته باشد.

اسکرول فرآیند کلی جابجایی درگاه نمایش است - یعنی "پنجره" محتوایی که به آن نگاه می کنید. هنگامی که پیمایش در هر دو محور x و y باشد، به آن پیمایش می گویند. برنامه نمونه InteractiveChart در این سند دو نوع مختلف اسکرول، کشیدن و پرت کردن را نشان می دهد:

  • کشیدن: این نوع پیمایشی است که زمانی اتفاق می‌افتد که کاربر انگشت خود را روی صفحه لمسی بکشد. می‌توانید کشیدن را با نادیده گرفتن onScroll() در GestureDetector.OnGestureListener پیاده‌سازی کنید. برای اطلاعات بیشتر در مورد کشیدن، به کشیدن و مقیاس نگاه کنید.
  • Flinging: این نوعی پیمایش است که زمانی رخ می دهد که کاربر انگشت خود را به سرعت بکشد و بلند کند. پس از اینکه کاربر انگشت خود را بلند کرد، عموماً می خواهید به حرکت درگاه نمایش ادامه دهید، اما سرعت را تا زمانی که درگاه نمایش متوقف شود، کاهش دهید. می‌توانید flinging را با نادیده گرفتن onFling() در GestureDetector.OnGestureListener و با استفاده از یک شی اسکرول پیاده‌سازی کنید.
  • Panning: پیمایش همزمان در امتداد هر دو محور x و y را پیمایش می‌گویند.

استفاده از اشیاء اسکرول همراه با ژست پرتاب معمول است، اما می‌توانید از آنها در هر زمینه‌ای که می‌خواهید رابط کاربری اسکرول را در پاسخ به یک رویداد لمسی نمایش دهد، استفاده کنید. برای مثال، می‌توانید onTouchEvent() را لغو کنید تا رویدادهای لمسی را مستقیماً پردازش کنید و در پاسخ به آن رویدادهای لمسی، یک افکت اسکرول یا یک انیمیشن «snap-to-page» تولید کنید.

مولفه هایی که شامل پیاده سازی های اسکرول داخلی هستند

اجزای Android زیر شامل پشتیبانی داخلی برای رفتار پیمایش و اسکرول است:

اگر برنامه شما نیاز به پشتیبانی از پیمایش و اسکرول در یک مؤلفه دیگر دارد، مراحل زیر را کامل کنید:

  1. یک پیاده‌سازی پیمایش مبتنی بر لمسی سفارشی ایجاد کنید .
  2. برای پشتیبانی از دستگاه‌هایی که Android 12 و جدیدتر را اجرا می‌کنند، جلوه کششی overscroll را اجرا کنید .

یک پیاده سازی اسکرول مبتنی بر لمس سفارشی ایجاد کنید

اگر برنامه شما از مؤلفه‌ای استفاده می‌کند که شامل پشتیبانی داخلی برای اسکرول و اسکرول نیست، این بخش نحوه ایجاد پیمایش خود را توضیح می‌دهد.

قطعه زیر از نمونه InteractiveChart می آید. از یک GestureDetector استفاده می کند و متد onFling() GestureDetector.SimpleOnGestureListener را لغو می کند. از OverScroller برای ردیابی ژست پرت کردن استفاده می کند. اگر کاربر پس از انجام ژست پرت کردن به لبه های محتوا برسد، ظرف زمانی را نشان می دهد که کاربر به پایان محتوا می رسد. نشانه بستگی به نسخه اندرویدی دارد که دستگاه اجرا می کند:

  • در اندروید 12 و نسخه‌های جدیدتر، عناصر بصری کشیده شده و به عقب باز می‌گردند.
  • در اندروید 11 و نسخه‌های قبلی، عناصر بصری جلوه‌ای درخشش نشان می‌دهند.

قسمت اول قطعه زیر اجرای onFling() را نشان می دهد:

کاتلین

// Viewport extremes. See currentViewport for a discussion of the viewport.
private val AXIS_X_MIN = -1f
private val AXIS_X_MAX = 1f
private val AXIS_Y_MIN = -1f
private val AXIS_Y_MAX = 1f

// The current viewport. This rectangle represents the visible chart
// domain and range. The viewport is the part of the app that the
// user manipulates via touch gestures.
private val currentViewport = RectF(AXIS_X_MIN, AXIS_Y_MIN, AXIS_X_MAX, AXIS_Y_MAX)

// The current destination rectangle—in pixel coordinates—into which
// the chart data must be drawn.
private lateinit var contentRect: Rect

private lateinit var scroller: OverScroller
private lateinit var scrollerStartViewport: RectF
...
private val gestureListener = object : GestureDetector.SimpleOnGestureListener() {

    override fun onDown(e: MotionEvent): Boolean {
        // Initiates the decay phase of any active edge effects.
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
            releaseEdgeEffects()
        }
        scrollerStartViewport.set(currentViewport)
        // Aborts any active scroll animations and invalidates.
        scroller.forceFinished(true)
        ViewCompat.postInvalidateOnAnimation(this@InteractiveLineGraphView)
        return true
    }
    ...
    override fun onFling(
            e1: MotionEvent,
            e2: MotionEvent,
            velocityX: Float,
            velocityY: Float
    ): Boolean {
        fling((-velocityX).toInt(), (-velocityY).toInt())
        return true
    }
}

private fun fling(velocityX: Int, velocityY: Int) {
    // Initiates the decay phase of any active edge effects.
    // On Android 12 and later, the edge effect (stretch) must
    // continue.
    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
            releaseEdgeEffects()
    }
    // Flings use math in pixels, as opposed to math based on the viewport.
    val surfaceSize: Point = computeScrollSurfaceSize()
    val (startX: Int, startY: Int) = scrollerStartViewport.run {
        set(currentViewport)
        (surfaceSize.x * (left - AXIS_X_MIN) / (AXIS_X_MAX - AXIS_X_MIN)).toInt() to
                (surfaceSize.y * (AXIS_Y_MAX - bottom) / (AXIS_Y_MAX - AXIS_Y_MIN)).toInt()
    }
    // Before flinging, stops the current animation.
    scroller.forceFinished(true)
    // Begins the animation.
    scroller.fling(
            // Current scroll position.
            startX,
            startY,
            velocityX,
            velocityY,
            /*
             * Minimum and maximum scroll positions. The minimum scroll
             * position is generally 0 and the maximum scroll position
             * is generally the content size less the screen size. So if the
             * content width is 1000 pixels and the screen width is 200
             * pixels, the maximum scroll offset is 800 pixels.
             */
            0, surfaceSize.x - contentRect.width(),
            0, surfaceSize.y - contentRect.height(),
            // The edges of the content. This comes into play when using
            // the EdgeEffect class to draw "glow" overlays.
            contentRect.width() / 2,
            contentRect.height() / 2
    )
    // Invalidates to trigger computeScroll().
    ViewCompat.postInvalidateOnAnimation(this)
}

جاوا

// Viewport extremes. See currentViewport for a discussion of the viewport.
private static final float AXIS_X_MIN = -1f;
private static final float AXIS_X_MAX = 1f;
private static final float AXIS_Y_MIN = -1f;
private static final float AXIS_Y_MAX = 1f;

// The current viewport. This rectangle represents the visible chart
// domain and range. The viewport is the part of the app that the
// user manipulates via touch gestures.
private RectF currentViewport =
  new RectF(AXIS_X_MIN, AXIS_Y_MIN, AXIS_X_MAX, AXIS_Y_MAX);

// The current destination rectangle—in pixel coordinates—into which
// the chart data must be drawn.
private final Rect contentRect = new Rect();

private final OverScroller scroller;
private final RectF scrollerStartViewport =
  new RectF(); // Used only for zooms and flings.
...
private final GestureDetector.SimpleOnGestureListener gestureListener
        = new GestureDetector.SimpleOnGestureListener() {
    @Override
    public boolean onDown(MotionEvent e) {
         if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
            releaseEdgeEffects();
        }
        scrollerStartViewport.set(currentViewport);
        scroller.forceFinished(true);
        ViewCompat.postInvalidateOnAnimation(InteractiveLineGraphView.this);
        return true;
    }
...
    @Override
    public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
        fling((int) -velocityX, (int) -velocityY);
        return true;
    }
};

private void fling(int velocityX, int velocityY) {
    // Initiates the decay phase of any active edge effects.
    // On Android 12 and later, the edge effect (stretch) must
    // continue.
    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
            releaseEdgeEffects();
    }
    // Flings use math in pixels, as opposed to math based on the viewport.
    Point surfaceSize = computeScrollSurfaceSize();
    scrollerStartViewport.set(currentViewport);
    int startX = (int) (surfaceSize.x * (scrollerStartViewport.left -
            AXIS_X_MIN) / (
            AXIS_X_MAX - AXIS_X_MIN));
    int startY = (int) (surfaceSize.y * (AXIS_Y_MAX -
            scrollerStartViewport.bottom) / (
            AXIS_Y_MAX - AXIS_Y_MIN));
    // Before flinging, stops the current animation.
    scroller.forceFinished(true);
    // Begins the animation.
    scroller.fling(
            // Current scroll position.
            startX,
            startY,
            velocityX,
            velocityY,
            /*
             * Minimum and maximum scroll positions. The minimum scroll
             * position is generally 0 and the maximum scroll position
             * is generally the content size less the screen size. So if the
             * content width is 1000 pixels and the screen width is 200
             * pixels, the maximum scroll offset is 800 pixels.
             */
            0, surfaceSize.x - contentRect.width(),
            0, surfaceSize.y - contentRect.height(),
            // The edges of the content. This comes into play when using
            // the EdgeEffect class to draw "glow" overlays.
            contentRect.width() / 2,
            contentRect.height() / 2);
    // Invalidates to trigger computeScroll().
    ViewCompat.postInvalidateOnAnimation(this);
}

هنگامی که onFling() postInvalidateOnAnimation() را فراخوانی می کند، computeScroll() برای به روز رسانی مقادیر x و y فعال می کند. همانطور که در مثال قبل نشان داده شده است، این معمولاً زمانی انجام می‌شود که یک view child با استفاده از یک شیء پیمایشی، یک اسکرول را متحرک می‌کند.

اکثر نماها موقعیت x و y شی اسکرولر را مستقیماً به scrollTo() منتقل می کنند. پیاده‌سازی computeScroll() رویکرد متفاوتی دارد: computeScrollOffset() را برای بدست آوردن مکان فعلی x و y فراخوانی می‌کند. وقتی معیارهای نمایش افکت لبه "درخشش" overscroll برآورده می شود - یعنی نمایشگر بزرگنمایی شده است، x یا y خارج از محدوده است و برنامه قبلاً یک overscroll را نشان نمی دهد - کد درخشش overscroll را تنظیم می کند. افکت کرده و postInvalidateOnAnimation() فراخوانی می کند تا یک invalidate در view ایجاد کند.

کاتلین

// Edge effect/overscroll tracking objects.
private lateinit var edgeEffectTop: EdgeEffect
private lateinit var edgeEffectBottom: EdgeEffect
private lateinit var edgeEffectLeft: EdgeEffect
private lateinit var edgeEffectRight: EdgeEffect

private var edgeEffectTopActive: Boolean = false
private var edgeEffectBottomActive: Boolean = false
private var edgeEffectLeftActive: Boolean = false
private var edgeEffectRightActive: Boolean = false

override fun computeScroll() {
    super.computeScroll()

    var needsInvalidate = false

    // The scroller isn't finished, meaning a fling or
    // programmatic pan operation is active.
    if (scroller.computeScrollOffset()) {
        val surfaceSize: Point = computeScrollSurfaceSize()
        val currX: Int = scroller.currX
        val currY: Int = scroller.currY

        val (canScrollX: Boolean, canScrollY: Boolean) = currentViewport.run {
            (left > AXIS_X_MIN || right < AXIS_X_MAX) to (top > AXIS_Y_MIN || bottom < AXIS_Y_MAX)
        }

        /*
         * If you are zoomed in, currX or currY is
         * outside of bounds, and you aren't already
         * showing overscroll, then render the overscroll
         * glow edge effect.
         */
        if (canScrollX
                && currX < 0
                && edgeEffectLeft.isFinished
                && !edgeEffectLeftActive) {
            edgeEffectLeft.onAbsorb(scroller.currVelocity.toInt())
            edgeEffectLeftActive = true
            needsInvalidate = true
        } else if (canScrollX
                && currX > surfaceSize.x - contentRect.width()
                && edgeEffectRight.isFinished
                && !edgeEffectRightActive) {
            edgeEffectRight.onAbsorb(scroller.currVelocity.toInt())
            edgeEffectRightActive = true
            needsInvalidate = true
        }

        if (canScrollY
                && currY < 0
                && edgeEffectTop.isFinished
                && !edgeEffectTopActive) {
            edgeEffectTop.onAbsorb(scroller.currVelocity.toInt())
            edgeEffectTopActive = true
            needsInvalidate = true
        } else if (canScrollY
                && currY > surfaceSize.y - contentRect.height()
                && edgeEffectBottom.isFinished
                && !edgeEffectBottomActive) {
            edgeEffectBottom.onAbsorb(scroller.currVelocity.toInt())
            edgeEffectBottomActive = true
            needsInvalidate = true
        }
        ...
    }
}

جاوا

// Edge effect/overscroll tracking objects.
private EdgeEffectCompat edgeEffectTop;
private EdgeEffectCompat edgeEffectBottom;
private EdgeEffectCompat edgeEffectLeft;
private EdgeEffectCompat edgeEffectRight;

private boolean edgeEffectTopActive;
private boolean edgeEffectBottomActive;
private boolean edgeEffectLeftActive;
private boolean edgeEffectRightActive;

@Override
public void computeScroll() {
    super.computeScroll();

    boolean needsInvalidate = false;

    // The scroller isn't finished, meaning a fling or
    // programmatic pan operation is active.
    if (scroller.computeScrollOffset()) {
        Point surfaceSize = computeScrollSurfaceSize();
        int currX = scroller.getCurrX();
        int currY = scroller.getCurrY();

        boolean canScrollX = (currentViewport.left > AXIS_X_MIN
                || currentViewport.right < AXIS_X_MAX);
        boolean canScrollY = (currentViewport.top > AXIS_Y_MIN
                || currentViewport.bottom < AXIS_Y_MAX);

        /*
         * If you are zoomed in, currX or currY is
         * outside of bounds, and you aren't already
         * showing overscroll, then render the overscroll
         * glow edge effect.
         */
        if (canScrollX
                && currX < 0
                && edgeEffectLeft.isFinished()
                && !edgeEffectLeftActive) {
            edgeEffectLeft.onAbsorb((int)mScroller.getCurrVelocity());
            edgeEffectLeftActive = true;
            needsInvalidate = true;
        } else if (canScrollX
                && currX > (surfaceSize.x - contentRect.width())
                && edgeEffectRight.isFinished()
                && !edgeEffectRightActive) {
            edgeEffectRight.onAbsorb((int)mScroller.getCurrVelocity());
            edgeEffectRightActive = true;
            needsInvalidate = true;
        }

        if (canScrollY
                && currY < 0
                && edgeEffectTop.isFinished()
                && !edgeEffectTopActive) {
            edgeEffectRight.onAbsorb((int)mScroller.getCurrVelocity());
            edgeEffectTopActive = true;
            needsInvalidate = true;
        } else if (canScrollY
                && currY > (surfaceSize.y - contentRect.height())
                && edgeEffectBottom.isFinished()
                && !edgeEffectBottomActive) {
            edgeEffectRight.onAbsorb((int)mScroller.getCurrVelocity());
            edgeEffectBottomActive = true;
            needsInvalidate = true;
        }
        ...
    }

در اینجا بخشی از کد است که بزرگنمایی واقعی را انجام می دهد:

کاتلین

lateinit var zoomer: Zoomer
val zoomFocalPoint = PointF()
...
// If a zoom is in progress—either programmatically
// or through double touch—this performs the zoom.
if (zoomer.computeZoom()) {
    val newWidth: Float = (1f - zoomer.currZoom) * scrollerStartViewport.width()
    val newHeight: Float = (1f - zoomer.currZoom) * scrollerStartViewport.height()
    val pointWithinViewportX: Float =
            (zoomFocalPoint.x - scrollerStartViewport.left) / scrollerStartViewport.width()
    val pointWithinViewportY: Float =
            (zoomFocalPoint.y - scrollerStartViewport.top) / scrollerStartViewport.height()
    currentViewport.set(
            zoomFocalPoint.x - newWidth * pointWithinViewportX,
            zoomFocalPoint.y - newHeight * pointWithinViewportY,
            zoomFocalPoint.x + newWidth * (1 - pointWithinViewportX),
            zoomFocalPoint.y + newHeight * (1 - pointWithinViewportY)
    )
    constrainViewport()
    needsInvalidate = true
}
if (needsInvalidate) {
    ViewCompat.postInvalidateOnAnimation(this)
}

جاوا

// Custom object that is functionally similar to Scroller.
Zoomer zoomer;
private PointF zoomFocalPoint = new PointF();
...
// If a zoom is in progress—either programmatically
// or through double touch—this performs the zoom.
if (zoomer.computeZoom()) {
    float newWidth = (1f - zoomer.getCurrZoom()) *
            scrollerStartViewport.width();
    float newHeight = (1f - zoomer.getCurrZoom()) *
            scrollerStartViewport.height();
    float pointWithinViewportX = (zoomFocalPoint.x -
            scrollerStartViewport.left)
            / scrollerStartViewport.width();
    float pointWithinViewportY = (zoomFocalPoint.y -
            scrollerStartViewport.top)
            / scrollerStartViewport.height();
    currentViewport.set(
            zoomFocalPoint.x - newWidth * pointWithinViewportX,
            zoomFocalPoint.y - newHeight * pointWithinViewportY,
            zoomFocalPoint.x + newWidth * (1 - pointWithinViewportX),
            zoomFocalPoint.y + newHeight * (1 - pointWithinViewportY));
    constrainViewport();
    needsInvalidate = true;
}
if (needsInvalidate) {
    ViewCompat.postInvalidateOnAnimation(this);
}

این متد computeScrollSurfaceSize() است که در قطعه قبلی فراخوانی شده است. اندازه سطح قابل پیمایش فعلی را بر حسب پیکسل محاسبه می کند. به عنوان مثال، اگر کل منطقه نمودار قابل مشاهده است، این اندازه فعلی mContentRect است. اگر نمودار در هر دو جهت 200% بزرگ‌نمایی شود، اندازه برگشتی دو برابر بزرگ‌تر به صورت افقی و عمودی است.

کاتلین

private fun computeScrollSurfaceSize(): Point {
    return Point(
            (contentRect.width() * (AXIS_X_MAX - AXIS_X_MIN) / currentViewport.width()).toInt(),
            (contentRect.height() * (AXIS_Y_MAX - AXIS_Y_MIN) / currentViewport.height()).toInt()
    )
}

جاوا

private Point computeScrollSurfaceSize() {
    return new Point(
            (int) (contentRect.width() * (AXIS_X_MAX - AXIS_X_MIN)
                    / currentViewport.width()),
            (int) (contentRect.height() * (AXIS_Y_MAX - AXIS_Y_MIN)
                    / currentViewport.height()));
}

برای مثال دیگری از استفاده از اسکرول، کد منبع کلاس ViewPager را ببینید. در پاسخ به flings پیمایش می کند و از اسکرول برای اجرای انیمیشن "snap-to-page" استفاده می کند.

افکت کشش overscroll را اجرا کنید

با شروع اندروید 12، EdgeEffect API های زیر را برای اجرای افکت Overscroll کششی اضافه می کند:

  • getDistance()
  • onPullDistance()

برای ارائه بهترین تجربه کاربری با کشش overscroll، موارد زیر را انجام دهید:

  1. هنگامی که وقتی کاربر محتویات را لمس می کند انیمیشن کششی اعمال می شود، لمس را به عنوان "گیر" ثبت کنید. کاربر انیمیشن را متوقف می کند و دوباره شروع به دستکاری کشش می کند.
  2. وقتی کاربر انگشت خود را در جهت مخالف کشش حرکت می‌دهد، کشش را رها کنید تا کاملاً از بین برود و سپس پیمایش را شروع کنید.
  3. هنگامی که کاربر در حین کشش پرت می کند، EdgeEffect را برای افزایش افکت کشش پرتاب کنید.

انیمیشن را بگیرید

وقتی کاربر یک انیمیشن کششی فعال را می‌گیرد، EdgeEffect.getDistance() 0 را برمی‌گرداند. این حالت نشان می دهد که کشش باید با حرکت لمسی دستکاری شود. در اکثر کانتینرها، catch در onInterceptTouchEvent() شناسایی می شود، همانطور که در قطعه کد زیر نشان داده شده است:

کاتلین

override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
  ...
  when (action and MotionEvent.ACTION_MASK) {
    MotionEvent.ACTION_DOWN ->
      ...
      isBeingDragged = EdgeEffectCompat.getDistance(edgeEffectBottom) > 0f ||
          EdgeEffectCompat.getDistance(edgeEffectTop) > 0f
      ...
  }
  return isBeingDragged
}

جاوا

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
  ...
  switch (action & MotionEvent.ACTION_MASK) {
    case MotionEvent.ACTION_DOWN:
      ...
      isBeingDragged = EdgeEffectCompat.getDistance(edgeEffectBottom) > 0
          || EdgeEffectCompat.getDistance(edgeEffectTop) > 0;
      ...
  }
}

در مثال قبل، زمانی که mIsBeingDragged true باشد، onInterceptTouchEvent() true برمی گرداند، بنابراین کافی است رویداد را قبل از اینکه کودک فرصتی برای مصرف داشته باشد مصرف کنید.

افکت overscroll را رها کنید

مهم است که جلوه کشش را قبل از پیمایش آزاد کنید تا از اعمال کشش به محتوای اسکرول جلوگیری کنید. نمونه کد زیر این بهترین روش را اعمال می کند:

کاتلین

override fun onTouchEvent(ev: MotionEvent): Boolean {
  val activePointerIndex = ev.actionIndex

  when (ev.getActionMasked()) {
    MotionEvent.ACTION_MOVE ->
      val x = ev.getX(activePointerIndex)
      val y = ev.getY(activePointerIndex)
      var deltaY = y - lastMotionY
      val pullDistance = deltaY / height
      val displacement = x / width

      if (deltaY < 0f && EdgeEffectCompat.getDistance(edgeEffectTop) > 0f) {
        deltaY -= height * EdgeEffectCompat.onPullDistance(edgeEffectTop,
            pullDistance, displacement);
      }
      if (deltaY > 0f && EdgeEffectCompat.getDistance(edgeEffectBottom) > 0f) {
        deltaY += height * EdgeEffectCompat.onPullDistance(edgeEffectBottom,
            -pullDistance, 1 - displacement);
      }
      ...
  }

جاوا

@Override
public boolean onTouchEvent(MotionEvent ev) {

  final int actionMasked = ev.getActionMasked();

  switch (actionMasked) {
    case MotionEvent.ACTION_MOVE:
      final float x = ev.getX(activePointerIndex);
      final float y = ev.getY(activePointerIndex);
      float deltaY = y - lastMotionY;
      float pullDistance = deltaY / getHeight();
      float displacement = x / getWidth();

      if (deltaY < 0 && EdgeEffectCompat.getDistance(edgeEffectTop) > 0) {
        deltaY -= getHeight() * EdgeEffectCompat.onPullDistance(edgeEffectTop,
            pullDistance, displacement);
      }
      if (deltaY > 0 && EdgeEffectCompat.getDistance(edgeEffectBottom) > 0) {
        deltaY += getHeight() * EdgeEffectCompat.onPullDistance(edgeEffectBottom,
            -pullDistance, 1 - displacement);
      }
            ...

هنگامی که کاربر در حال کشیدن است، قبل از اینکه رویداد لمسی را به محفظه پیمایشی تو در تو منتقل کنید یا اسکرول را بکشید، فاصله کشش EdgeEffect مصرف کنید. در نمونه کد قبلی، getDistance() یک مقدار مثبت را هنگامی که یک افکت لبه نمایش داده می شود برمی گرداند و می تواند با حرکت آزاد شود. هنگامی که رویداد لمسی کشش را آزاد می کند، ابتدا توسط EdgeEffect مصرف می شود تا قبل از نمایش جلوه های دیگر، مانند پیمایش تو در تو، به طور کامل آزاد شود. می‌توانید از getDistance() استفاده کنید تا بفهمید چقدر فاصله کششی برای آزاد کردن اثر فعلی لازم است.

برخلاف onPull() ، onPullDistance() مقدار مصرف شده دلتای عبور شده را برمی گرداند. در اندروید 12، اگر onPull() یا onPullDistance() به مقادیر deltaDistance منفی منتقل شوند زمانی که getDistance() 0 باشد، افکت کشش تغییر نمی کند. در اندروید 11 و نسخه‌های قبلی، onPull() به مقادیر منفی برای کل مسافت اجازه می‌دهد جلوه‌های درخشندگی را نشان دهد.

از overscroll انصراف دهید

می‌توانید از overscroll در فایل طرح‌بندی خود یا به صورت برنامه‌نویسی انصراف دهید.

برای انصراف در فایل طرح بندی خود، android:overScrollMode مانند مثال زیر تنظیم کنید:

<MyCustomView android:overScrollMode="never">
    ...
</MyCustomView>

برای انصراف از طریق برنامه، از کدهایی مانند زیر استفاده کنید:

کاتلین

customView.overScrollMode = View.OVER_SCROLL_NEVER

جاوا

customView.setOverScrollMode(View.OVER_SCROLL_NEVER);

منابع اضافی

به منابع مرتبط زیر مراجعه کنید: