惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Google DeepMind News
Google DeepMind News
B
Blog RSS Feed
量子位
aimingoo的专栏
aimingoo的专栏
V
Visual Studio Blog
Y
Y Combinator Blog
Vercel News
Vercel News
云风的 BLOG
云风的 BLOG
宝玉的分享
宝玉的分享
Engineering at Meta
Engineering at Meta
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
GbyAI
GbyAI
人人都是产品经理
人人都是产品经理
博客园 - 叶小钗
Stack Overflow Blog
Stack Overflow Blog
大猫的无限游戏
大猫的无限游戏
Microsoft Security Blog
Microsoft Security Blog
B
Blog
Last Week in AI
Last Week in AI
有赞技术团队
有赞技术团队
博客园 - 聂微东
腾讯CDC
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
J
Java Code Geeks

Inside Nutrient

A guide to the invisible work behind documents Introducing Nutrient Documents for Salesforce: Native document generation and signing Document AI vs. traditional OCR: Choosing between OCR, AI, and hybrid pipelines PDF SDK compliance and security evaluation checklist for enterprise teams (2026) Invariant Corp replaces paper processes with Nutrient Workflow and scales without limits What is process mapping? A complete guide Nutrient vs. Conga Composer for Salesforce document generation (2026) Document routing: How to automate document distribution The CTO’s AI playbook: Why accountability architecture beats orchestration Compliance workflow automation: Why built-in compliance is table stakes Workflow diagrams: Examples, symbols, and how to build one that actually runs Digital forms: Replace paper forms with automated workflows Approval workflow software: How to automate approvals Why document-centric automation is different The CEO’s AI playbook: Why decision architecture beats model selection Nutrient SDK product updates for Q1 2026 PDF redaction verification: How to prove sensitive data is permanently removed What is a VPAT? The complete guide to accessibility conformance reports What is PDF/UA? The accessible PDF standard explained Salesforce eSignatures: Generate, sign, and track documents in one flow Online document viewer: Options, tradeoffs, and how to embed one Document viewer for web apps: React, Vue, Angular (2026) Best document viewers in 2026: A buyer’s guide How to edit a PDF in Python: Add text, images, and annotations Nutrient advances Workflow platform with agentic AI for enterprise-grade speed and consistency in document-heavy operations How to create a Salesforce quote template from opportunity data The business case for accessibility: Five ways it drives enterprise value Python PDF library comparison (2026): 7 libraries for developers Why your AI agent hallucinates PDF table data PDF.js limitations: When to upgrade to a commercial PDF SDK
Drag-to-reorder with Jetpack Compose
Ferdinand Bada · 2024-08-20 · via Inside Nutrient

Table of contents

    Drag-to-reorder with Jetpack Compose

    Nutrient Android SDK has plenty of screens that have been written in the traditional view system to achieve the smooth and powerful visuals currently on offer. However, since its introduction in 2019, the Android world has been mesmerized by Jetpack Compose(opens in a new tab), which offers a newer and easier way to build user interfaces (UIs) on Android. Here at Nutrient , we’ve also been gradually working to convert our UIs to take advantage of the power of Compose. But during the course of the conversion, we’ve encountered a number of issues that need custom solutions. One such example is the drag-to-reorder list feature, which we use in the bookmarks and annotations screens.

    Prior to Compose, drag-to-reorder of displayed items was built using a RecyclerView. This was done with the help of an ItemTouchHelper.Callback, which exposes methods that allow this functionality. However, in Compose, this isn’t straightforward, which is why we had to declare a custom Modifier.

    This custom modifier is only for vertical scrolling composables like Column and LazyColumn.

    Implementing the dragToReorder modifier

    The following steps will outline how we achieved this functionality.

    1. Declare an enum class that will hold the state of sliding at the moment — whether dragging up, dragging down, or not dragging:

    enum class SlideState { NONE, UP, DOWN }

    1. Declare an extension function for the Modifier we’ll add. This function has the following signature:

    fun <T> Modifier.dragToReorder(

    item: T, // The current item being dragged.

    itemList: List<T>, // The entire list of the items onscreen.

    itemHeight: Int, // The height of an item (in pixels).

    updateSlideState: (item: T, slideState: SlideState) -> Unit, // Callback to update the slide state of an item.

    onStartDrag: (currIndex: Int) -> Unit = {}, // Callback invoked when dragging begins and exposes the index of the item being dragged.

    onStopDrag: (currIndex: Int, destIndex: Int) -> Unit // Call invoked when drag is finished.

    ): Modifier = composed {

    // Magic goes here. See step 3.

    }

    1. Paste the following in the composed lambda:

    // Keep track of the of the animated vertical drag offset to allow for a smooth UI.

    val offsetY = remember { Animatable(0f) }

    // Handle pointer input for detecting gestures (in this case, drag).

    pointerInput(Unit) {

    // Wrap in a coroutine scope to use suspend functions for touch events and animation.

    coroutineScope {

    // The index of the current item in the list.

    val itemIndex = itemList.indexOf(item)

    // Threshold for when an item should be considered as moved to a new position in the list.

    // Needs to be at least a half of the height of the item, but this can be modified as needed.

    val offsetToSlide = itemHeight / 2

    // Variables to track the number of items slid over and the total offset.

    var numberOfSlidItems = 0

    var previousNumberOfItems: Int

    var listOffset = 0

    // Invoked when the dragging operation has started.

    val onDragStart = {

    // Interrupt any ongoing animation of other items.

    launch {

    offsetY.stop()

    }

    // Invoke the start drag callback with the current item's index.

    onStartDrag(itemIndex)

    }

    // Define the action to perform while dragging.

    val onDragging = { change: PointerInputChange ->

    // Calculate the new vertical drag offset.

    val verticalDragOffset = offsetY.value + change.positionChange().y

    launch {

    // Snap the offset to the calculated value.

    offsetY.snapTo(verticalDragOffset)

    // Determine the direction of the drag.

    val offsetSign = offsetY.value.sign.toInt()

    // Update the number of items slid over based on the drag offset.

    previousNumberOfItems = numberOfSlidItems

    numberOfSlidItems = calculateNumberOfSlidItems(

    offsetY.value * offsetSign,

    itemHeight,

    offsetToSlide,

    previousNumberOfItems

    )

    // Update the visual state of the items being slid over.

    if (previousNumberOfItems > numberOfSlidItems) {

    updateSlideState(

    itemList[itemIndex + previousNumberOfItems * offsetSign],

    SlideState.NONE

    )

    } else if (numberOfSlidItems != 0) {

    try {

    updateSlideState(

    itemList[itemIndex + numberOfSlidItems * offsetSign],

    if (offsetSign == 1) SlideState.UP else SlideState.DOWN

    )

    } catch (e: IndexOutOfBoundsException) {

    numberOfSlidItems = previousNumberOfItems

    }

    }

    // Update the total offset based on the number of items slid over.

    listOffset = numberOfSlidItems * offsetSign

    }

    // Consume the gesture event, not passed to external.

    if (change.positionChange() != androidx.compose.ui.geometry.Offset.Zero) change.consume()

    }

    // Define the action to perform when dragging ends.

    val onDragEnd = {

    launch {

    // Animate the vertical offset of the dragged item to its final position.

    // The final position is calculated based on the number of items slid over (`numberOfSlidItems`),

    // the height of each item (`itemHeight`), and the direction of the drag (`offsetY.value.sign`).

    offsetY.animateTo(itemHeight * numberOfSlidItems * offsetY.value.sign)

    // Invoke the stop drag callback with the current item's index and its final index.

    onStopDrag(itemIndex, itemIndex + listOffset)

    }

    }

    // Detect drag gestures after a long press, invoking the defined actions.

    detectDragGesturesAfterLongPress(

    onDragStart = { onDragStart() },

    onDrag = { change, _ -> onDragging(change) },

    onDragEnd = { onDragEnd() }

    )

    }

    }.offset {

    // Apply the calculated vertical offset using 0 for `x` because we are only interested in the `y` drag.

    IntOffset(0, offsetY.value.roundToInt())

    }

    1. The calculateNumberOfSlidItems method is defined below:

    private const val NO_ITEMS_MOVED = 0

    // Helper function to calculate the number of items that have been slid over based on the drag offset.

    private fun calculateNumberOfSlidItems(

    offsetY: Float, // The current vertical offset of the dragged item.

    itemHeight: Int, // The height of each item in the list.

    offsetToSlide: Int, // The minimum vertical distance needed to consider an item as moved.

    previousNumberOfItems: Int // The previously calculated number of items moved past.

    ): Int {

    // Calculate the number of items that would fit in the current vertical offset.

    val numberOfItemsInOffset = (offsetY / itemHeight).toInt()

    // Calculate the number of items considering the threshold for moving (`offsetToSlide`).

    // Adding `offsetToSlide` ensures we're looking slightly beyond the current offset to see if another item has been moved past.

    val numberOfItemsPlusOffset = ((offsetY + offsetToSlide) / itemHeight).toInt()

    // Calculate the number of items considering the threshold for moving (`offsetToSlide`).

    // Subtracting `offsetToSlide` - 1 ensures we're looking slightly before the current offset to account for potential rounding errors.

    // The subtraction of 1 is to ensure that we don't prematurely count an item as moved when it's very close but hasn't quite reached the threshold.

    // This number can be tweaked to achieve different behaviors depending on the need.

    val numberOfItemsMinusOffset = ((offsetY - offsetToSlide - 1) / itemHeight).toInt()

    // Determine the number of items moved past based on the calculated values.

    return when {

    offsetY - offsetToSlide - 1 < 0 -> NO_ITEMS_MOVED

    numberOfItemsPlusOffset > numberOfItemsInOffset -> numberOfItemsPlusOffset

    numberOfItemsMinusOffset < numberOfItemsInOffset -> numberOfItemsInOffset

    else -> previousNumberOfItems

    }

    }

    The Modifier we just created works as shown below when used in a simple sample project, which can be found in this GitHub repository(opens in a new tab).

    Wrapping up

    With the Modifier shown above, we were able to create a custom and extensible solution that’s reusable and highly customizable. The Modifier can also be modified to, for example, expose a callback that will be invoked during the dragging operation itself as opposed to when the dragging starts and stops. This approach also helps us keep third-party dependencies to a minimum, in turn reducing complexity in the Nutrient Android SDK.

    As always, our Sales and Solutions Engineering teams are available to assist you. Whether you have inquiries about upgrading your existing Nutrient solutions or you’re interested in becoming a new customer, we’re here to help.

    Additional reading

    In the meantime, if you want to learn more about Jetpack Compose, check out the following blogs:

    Explore related topics

    Try for free Ready to get started?

    Related SDK articles

    Explore more