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

روش نوشتن را امتحان کنید
Jetpack Compose ابزار رابط کاربری پیشنهادی برای اندروید است. یاد بگیرید که چگونه از لمس و ورودی در Compose استفاده کنید.

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

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

  • از اندروید ۱۲ (سطح API ۳۱)، عناصر بصری با رویداد کشیدن (drag) کشیده شده و به حالت اولیه برمی‌گردند و با رویداد پرتاب (fling) به حالت اولیه برمی‌گردند و دوباره کشیده می‌شوند.
  • در اندروید ۱۱ (سطح API 30) و قبل از آن، مرزها پس از کشیدن یا رها کردن به لبه، جلوه‌ای «درخشش» نشان می‌دهند.

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

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

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

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

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

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

استفاده از اشیاء scroller همراه با یک حرکت fling رایج است، اما می‌توانید از آنها در هر زمینه‌ای که می‌خواهید رابط کاربری در پاسخ به یک رویداد لمسی، پیمایش را نمایش دهد، استفاده کنید. برای مثال، می‌توانید onTouchEvent() را برای پردازش مستقیم رویدادهای لمسی و تولید یک جلوه پیمایش یا یک انیمیشن "snap-to-page" در پاسخ به آن رویدادهای لمسی، بازنویسی کنید.

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

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

اگر برنامه شما نیاز به پشتیبانی از پیمایش و پیمایش بیش از حد در داخل یک کامپوننت دیگر دارد، مراحل زیر را انجام دهید:

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

ایجاد یک پیاده‌سازی پیمایش لمسی سفارشی

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

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

  • در اندروید ۱۲ و بالاتر، عناصر بصری کشیده می‌شوند و به حالت اولیه خود برمی‌گردند.
  • در اندروید ۱۱ و قبل از آن، عناصر بصری جلوه‌ای درخشان از خود نشان می‌دهند.

بخش اول قطعه کد زیر، پیاده‌سازی 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 در حال متحرک‌سازی یک scroll با استفاده از یک شیء scroller است، همانطور که در مثال قبل نشان داده شده است.

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

کاتلین

// 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 است. اگر نمودار در هر دو جهت ۲۰۰٪ بزرگنمایی شود، اندازه برگردانده شده دو برابر بزرگتر از حالت افقی و عمودی خواهد بود.

کاتلین

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()));
}

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

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

با شروع از اندروید ۱۲، EdgeEffect رابط‌های برنامه‌نویسی کاربردی (API) زیر را برای پیاده‌سازی افکت اسکرول کششی اضافه می‌کند:

  • getDistance()
  • onPullDistance()

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

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

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

وقتی کاربر یک انیمیشن کششی فعال را دریافت می‌کند، EdgeEffect.getDistance() 0 را برمی‌گرداند. این شرط نشان می‌دهد که کشش باید توسط حرکت لمسی دستکاری شود. در اکثر کانتینرها، دریافت در 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;
      ...
  }
}

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

اثر پیمایش بیش از حد را آزاد کنید

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

کاتلین

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() مقدار مصرف شده دلتای ارسالی را برمی‌گرداند. از اندروید ۱۲ به بعد، اگر onPull() یا onPullDistance() مقادیر منفی deltaDistance هنگام getDistance() برابر با 0 دریافت کنند، اثر کشش تغییر نمی‌کند. در اندروید ۱۱ و قبل از آن، onPull() اجازه می‌دهد مقادیر منفی برای کل فاصله، جلوه‌های درخشش را نشان دهند.

انصراف از پیمایش بیش از حد

شما می‌توانید در فایل طرح‌بندی خود یا به صورت برنامه‌نویسی، از پیمایش بیش از حد (overscroll) خودداری کنید.

برای لغو این قابلیت، در فایل طرح‌بندی خود، android:overScrollMode همانطور که در مثال زیر نشان داده شده است، تنظیم کنید:

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

برای لغو برنامه‌ریزی‌شده، از کدی مانند کد زیر استفاده کنید:

کاتلین

customView.overScrollMode = View.OVER_SCROLL_NEVER

جاوا

customView.setOverScrollMode(View.OVER_SCROLL_NEVER);

منابع اضافی

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