پویانمایی اشاره پیمایش

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

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

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

  • از Android 12 (سطح API‏ ۳۱) به بعد، عناصر بصری در رویداد کشیدن کشیده می‌شوند و به عقب برمی‌گردند و در رویداد پرتاب کردن پرتاب می‌شوند و به عقب برمی‌گردند.
  • در Android 11 (میانای برنامه‌سازی کاربردی سطح ۳۰) و نسخه‌های قبلی، مرزها پس‌از اشاره کشیدن یا پرتاب کردن به لبه، جلوه «درخشش» را نمایش می‌دهند.

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

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

آشنایی با اصطلاحات پیمایش

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

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

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

استفاده از اشیای پیمایشگر در ترکیب با اشاره تندزنی رایج است، اما می‌توانید از آن‌ها در هر زمینه‌ای که می‌خواهید میانای کاربری پیمایش را در پاسخ به رویداد لمسی نمایش دهد استفاده کنید. برای مثال، می‌توانید onTouchEvent() را لغو کنید تا رویدادهای لمسی را مستقیماً پردازش کنید و جلوه پیمایش یا پویانمایی «چسبیدن به صفحه» را در پاسخ به آن رویدادهای لمسی ایجاد کنید.

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

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

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

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

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

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

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

  • در Android 12 و نسخه‌های بالاتر، عناصر دیداری کشیده می‌شوند و به‌جای خود برمی‌گردند.
  • در Android 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 را به‌روز کند. این کار معمولاً زمانی انجام می‌شود که یک نمای فرزند درحال پویانمایی پیمایش بااستفاده از شیء پیمایشگر باشد، همان‌طور که در مثال قبلی نشان داده شده است.

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

کاتلین

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

برای نمونه دیگری از استفاده از پیمایشگر، کد منبع کلاس ViewPager را ببینید. این نما در پاسخ به حرکت‌های سریع پیمایش می‌کند و از پیمایش برای پیاده‌سازی پویانمایی «پرش به صفحه» استفاده می‌کند.

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

از Android 12،‏ EdgeEffect میاناهای برنامه‌سازی کاربردی زیر را برای پیاده‌سازی جلوه کشش فراتر از پیمایش اضافه می‌کند:

  • getDistance()
  • onPullDistance()

برای ارائه بهترین تجربه کاربری با پیمایش بیش‌ازحد کششی، کارهای زیر را انجام دهید:

  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;
      ...
  }
}

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

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

می‌توانید در فایل چیدمان یا به‌صورت برنامه‌نویسی‌شده از پیمایش بیش‌ازحد انصراف دهید.

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

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

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

کاتلین

customView.overScrollMode = View.OVER_SCROLL_NEVER

جاوا

customView.setOverScrollMode(View.OVER_SCROLL_NEVER);

منابع بیشتر

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