hololens开发之placeable.cs的脚本注释

using System.Collections.Generic;
using UnityEngine;
using Academy.HoloToolkit.Unity;

/// <summary>
/// Enumeration containing the surfaces on which a GameObject
/// can be placed.  For simplicity of this sample, only one
/// surface type is allowed to be selected.
/// </summary>
/// 枚举包含了可以被放置的物体的表面。为了简化例子,只有一种surface类型可以被选择
public enum PlacementSurfaces
{
    // Horizontal surface with an upward pointing normal.  有一个法线指向上的水平表面  
    Horizontal = 1,

    // Vertical surface with a normal facing the user. 有一个法线指向使用者的垂直表面  
    Vertical = 2,
}

/// <summary>
/// The Placeable class implements the logic used to determine if a GameObject
/// can be placed on a target surface. Constraints for placement include:
/// * No part of the GameObject's box collider impacts with another object in the scene
/// * The object lays flat (within specified tolerances) against the surface
/// * The object would not fall off of the surface if gravity were enabled.
/// This class also provides the following visualizations.
/// * A transparent cube representing the object's box collider.
/// * Shadow on the target surface indicating whether or not placement is valid.
/// </summary>
/// Placeable 类执行这样的逻辑:决定一个物体是否能被放置在一个目标表面上。放置的限制包括:
/// 1 这个物体的box collider不会影响场景中的另一个物体
/// 2 这个物体必须躺在平坦的表面上
/// 3 如果有重力存在,那么这个物体不会掉下来
/// 这个类也提供了下面的可视化
/// 一个透明的正方形代表了这个目标物体的box collider
/// 在目标surface上的阴影代表了能不能被放置
public class Placeable : MonoBehaviour
{
    [Tooltip("The base material used to render the bounds asset when placement is allowed.")]
    public Material PlaceableBoundsMaterial = null;

    [Tooltip("The base material used to render the bounds asset when placement is not allowed.")]
    public Material NotPlaceableBoundsMaterial = null;

    [Tooltip("The material used to render the placement shadow when placement it allowed.")]
    public Material PlaceableShadowMaterial = null;

    [Tooltip("The material used to render the placement shadow when placement it not allowed.")]
    public Material NotPlaceableShadowMaterial = null;

    //只有水平的surface可以被放置
    [Tooltip("The type of surface on which the object can be placed.")]
    public PlacementSurfaces PlacementSurface = PlacementSurfaces.Horizontal;

    //再放置的过程中,这个物体的子物体将被隐藏起来
    [Tooltip("The child object(s) to hide during placement.")]
    public List<GameObject> ChildrenToHide = new List<GameObject>();

    /// <summary>
    /// Indicates if the object is in the process of being placed.
    /// 布尔变量:这个目标物体是否正在被移动
    /// </summary>
    public bool IsPlacing { get; private set; }

    // The most recent distance to the surface.  This is used to 
    // locate the object when the user's gaze does not intersect
    // with the Spatial Mapping mesh.
    //离平面的最远距离。用来当用户的目光没有与空间网格进行交会时使用
    private float lastDistance = 2.0f;

    // The distance away from the target surface that the object should hover prior while being placed.
    //目标物体应该放在目标平面前的距离
    private float hoverDistance = 0.15f;

    // Threshold (the closer to 0, the stricter the standard) used to determine if a surface is flat.
    //阈值(接近0,更严格的标准)用于确定一个表面是平的。
    private float distanceThreshold = 0.02f;

    // Threshold (the closer to 1, the stricter the standard) used to determine if a surface is vertical.
    //阈值(接近0,更严格的标准)用于确定一个表面是垂直的。
    private float upNormalThreshold = 0.9f;

    // Maximum distance, from the object, that placement is allowed.
    // This is used when raycasting to see if the object is near a placeable surface.
    //当光线投射时看是否这个目标物体接近一个可以被放置的表面,最大放置距离
    private float maximumPlacementDistance = 5.0f;

    // Speed (1.0 being fastest) at which the object settles to the surface upon placement.
    //目标物体在位置上移动时的速度
    private float placementVelocity = 0.06f;

    // Indicates whether or not this script manages the object's box collider.
    //判断这个脚本是不是去管理这个目标物体的box colloider
    private bool managingBoxCollider = false;

    // The box collider used to determine of the object will fit in the desired location.
    // It is also used to size the bounding cube.
    //去决定目标物体是否符合期待的位置的box collider
    private BoxCollider boxCollider = null;

    // Visible asset used to show the dimensions of the object. This asset is sized
    // using the box collider's bounds.
    //可视化的资源去展示目标物体的大小。这个资源使用box collider的边界去表示大小
    private GameObject boundsAsset = null;

    // Visible asset used to show the where the object is attempting to be placed.
    // This asset is sized using the box collider's bounds.
    //可视话的资源去展示目标物体是不是正在企图被放置。这个资源使用box collider的边界去表示大小
    private GameObject shadowAsset = null;

    // The location at which the object will be placed.
    //目标物体将要被放置的位置
    private Vector3 targetPosition;

    /// <summary>
    /// Called when the GameObject is created.
    /// </summary>
    private void Awake()
    {
        targetPosition = gameObject.transform.position;

        // Get the object's collider. 没有box collider那就给他加一个box collider属性
        boxCollider = gameObject.GetComponent<BoxCollider>();
        if (boxCollider == null)
        {
            // The object does not have a collider, create one and remember that
            // we are managing it.
            managingBoxCollider = true;
            boxCollider = gameObject.AddComponent<BoxCollider>();
            boxCollider.enabled = false;
        }

        // Create the object that will be used to indicate the bounds of the GameObject.
        //设置一个方框之后,把它设置为目标物体的子物体,之后并不激活它
        boundsAsset = GameObject.CreatePrimitive(PrimitiveType.Cube);
        boundsAsset.transform.parent = gameObject.transform;
        boundsAsset.SetActive(false);

        // Create a object that will be used as a shadow.
        //弄一个方形的平板当作是目标物体的子物体(阴影),之后并不激活它
        shadowAsset = GameObject.CreatePrimitive(PrimitiveType.Quad);
        shadowAsset.transform.parent = gameObject.transform;
        shadowAsset.SetActive(false);
    }

    /// <summary>
    /// Called when our object is selected.  Generally called by
    /// a gesture management component.
    /// </summary>
    /// 当目标物体被选中时执行的函数
    public void OnSelect()
    {
        /* TODO: 4.a CODE ALONG 4.a */

        if (!IsPlacing)
        {
            OnPlacementStart();
        }
        else
        {
            OnPlacementStop();
        }
    }

    /// <summary>
    /// Called once per frame.
    /// </summary>
    private void Update()
    {
        /* TODO: 4.a CODE ALONG 4.a */

        if (IsPlacing)
        {
            // Move the object.
            Move();

            // Set the visual elements.
            Vector3 targetPosition;
            Vector3 surfaceNormal;
            bool canBePlaced = ValidatePlacement(out targetPosition, out surfaceNormal);
            DisplayBounds(canBePlaced);
            DisplayShadow(targetPosition, surfaceNormal, canBePlaced);
        }
        else
        {
            // Disable the visual elements.
            boundsAsset.SetActive(false);
            shadowAsset.SetActive(false);

            // Gracefully place the object on the target surface.
            float dist = (gameObject.transform.position - targetPosition).magnitude;
            if (dist > 0)
            {
                gameObject.transform.position = Vector3.Lerp(gameObject.transform.position, targetPosition, placementVelocity / dist);
            }
            else
            {
                // Unhide the child object(s) to make placement easier.
                for (int i = 0; i < ChildrenToHide.Count; i++)
                {
                    ChildrenToHide[i].SetActive(true);
                }
            }
        }
    }

    /// <summary>
    /// Verify whether or not the object can be placed.
    /// </summary>
    /// <param name="position">
    /// The target position on the surface.
    /// </param>
    /// <param name="surfaceNormal">
    /// The normal of the surface on which the object is to be placed.
    /// </param>
    /// <returns>
    /// True if the target position is valid for placing the object, otherwise false.
    /// </returns>
    private bool ValidatePlacement(out Vector3 position, out Vector3 surfaceNormal)
    {
        Vector3 raycastDirection = gameObject.transform.forward;

        if (PlacementSurface == PlacementSurfaces.Horizontal)
        {
            // Placing on horizontal surfaces.
            // Raycast from the bottom face of the box collider.
            raycastDirection = -(Vector3.up);
        }

        // Initialize out parameters.
        position = Vector3.zero;
        surfaceNormal = Vector3.zero;

        Vector3[] facePoints = GetColliderFacePoints();

        // The origin points we receive are in local space and we 
        // need to raycast in world space.
        for (int i = 0; i < facePoints.Length; i++)
        {
            facePoints[i] = gameObject.transform.TransformVector(facePoints[i]) + gameObject.transform.position;
        }

        // Cast a ray from the center of the box collider face to the surface.
        RaycastHit centerHit;
        if (!Physics.Raycast(facePoints[0],
                        raycastDirection,
                        out centerHit,
                        maximumPlacementDistance,
                        SpatialMappingManager.Instance.LayerMask))
        {
            // If the ray failed to hit the surface, we are done.
            return false;
        }

        // We have found a surface.  Set position and surfaceNormal.
        position = centerHit.point;
        surfaceNormal = centerHit.normal;

        // Cast a ray from the corners of the box collider face to the surface.
        for (int i = 1; i < facePoints.Length; i++)
        {
            RaycastHit hitInfo;
            if (Physics.Raycast(facePoints[i],
                                raycastDirection,
                                out hitInfo,
                                maximumPlacementDistance,
                                SpatialMappingManager.Instance.LayerMask))
            {
                // To be a valid placement location, each of the corners must have a similar
                // enough distance to the surface as the center point
                if (!IsEquivalentDistance(centerHit.distance, hitInfo.distance))
                {
                    return false;
                }
            }
            else
            {
                // The raycast failed to intersect with the target layer.
                return false;
            }
        }

        return true;
    }

    /// <summary>
    /// Determine the coordinates, in local space, of the box collider face that 
    /// will be placed against the target surface.
    /// </summary>
    /// <returns>
    /// Vector3 array with the center point of the face at index 0.
    /// </returns>
    private Vector3[] GetColliderFacePoints()
    {
        // Get the collider extents.  
        // The size values are twice the extents.
        Vector3 extents = boxCollider.size / 2;

        // Calculate the min and max values for each coordinate.
        float minX = boxCollider.center.x - extents.x;
        float maxX = boxCollider.center.x + extents.x;
        float minY = boxCollider.center.y - extents.y;
        float maxY = boxCollider.center.y + extents.y;
        float minZ = boxCollider.center.z - extents.z;
        float maxZ = boxCollider.center.z + extents.z;

        Vector3 center;
        Vector3 corner0;
        Vector3 corner1;
        Vector3 corner2;
        Vector3 corner3;

        if (PlacementSurface == PlacementSurfaces.Horizontal)
        {
            // Placing on horizontal surfaces.
            center = new Vector3(boxCollider.center.x, minY, boxCollider.center.z);
            corner0 = new Vector3(minX, minY, minZ);
            corner1 = new Vector3(minX, minY, maxZ);
            corner2 = new Vector3(maxX, minY, minZ);
            corner3 = new Vector3(maxX, minY, maxZ);
        }
        else
        {
            // Placing on vertical surfaces.
            center = new Vector3(boxCollider.center.x, boxCollider.center.y, maxZ);
            corner0 = new Vector3(minX, minY, maxZ);
            corner1 = new Vector3(minX, maxY, maxZ);
            corner2 = new Vector3(maxX, minY, maxZ);
            corner3 = new Vector3(maxX, maxY, maxZ);
        }

        return new Vector3[] { center, corner0, corner1, corner2, corner3 };
    }

    /// <summary>
    /// Put the object into placement mode.
    /// </summary>
    public void OnPlacementStart()
    {
        // If we are managing the collider, enable it. 开始移动之后,那么boxcollider显示
        if (managingBoxCollider)
        {
            boxCollider.enabled = true;
        }

        // Hide the child object(s) to make placement easier.隐藏子物体
        for (int i = 0; i < ChildrenToHide.Count; i++)
        {
            ChildrenToHide[i].SetActive(false);
        }

        // Tell the gesture manager that it is to assume
        // all input is to be given to this object.
        GestureManager.Instance.OverrideFocusedObject = gameObject;

        // Enter placement mode.表示被移动
        IsPlacing = true;
    }

    /// <summary>
    /// Take the object out of placement mode.
    /// </summary>
    /// <remarks>
    /// This method will leave the object in placement mode if called while
    /// the object is in an invalid location.  To determine whether or not
    /// the object has been placed, check the value of the IsPlacing property.
    /// </remarks>
    public void OnPlacementStop()
    {
        // ValidatePlacement requires a normal as an out parameter.
        Vector3 position;
        Vector3 surfaceNormal;

        // Check to see if we can exit placement mode.
        if (!ValidatePlacement(out position, out surfaceNormal))
        {
            return;
        }

        // The object is allowed to be placed.
        // We are placing at a small buffer away from the surface.
        targetPosition = position + (0.01f * surfaceNormal);

        OrientObject(true, surfaceNormal);

        // If we are managing the collider, disable it. 
        if (managingBoxCollider)
        {
            boxCollider.enabled = false;
        }

        // Tell the gesture manager that it is to resume
        // its normal behavior.
        GestureManager.Instance.OverrideFocusedObject = null;

        // Exit placement mode.
        IsPlacing = false;
    }

    /// <summary>
    /// Positions the object along the surface toward which the user is gazing.
    /// </summary>
    /// <remarks>
    /// If the user's gaze does not intersect with a surface, the object
    /// will remain at the most recently calculated distance.
    /// </remarks>
    private void Move()
    {
        Vector3 moveTo = gameObject.transform.position;
        Vector3 surfaceNormal = Vector3.zero;
        RaycastHit hitInfo;

        bool hit = Physics.Raycast(Camera.main.transform.position,
                                Camera.main.transform.forward,
                                out hitInfo,
                                20f,
                                SpatialMappingManager.Instance.LayerMask);

        if (hit)
        {
            float offsetDistance = hoverDistance;

            // Place the object a small distance away from the surface while keeping 
            // the object from going behind the user.
            if (hitInfo.distance <= hoverDistance)
            {
                offsetDistance = 0f;
            }

            moveTo = hitInfo.point + (offsetDistance * hitInfo.normal);

            lastDistance = hitInfo.distance;
            surfaceNormal = hitInfo.normal;
        }
        else
        {
            // The raycast failed to hit a surface.  In this case, keep the object at the distance of the last
            // intersected surface.
            moveTo = Camera.main.transform.position + (Camera.main.transform.forward * lastDistance);
        }

        // Follow the user's gaze.
        float dist = Mathf.Abs((gameObject.transform.position - moveTo).magnitude);
        gameObject.transform.position = Vector3.Lerp(gameObject.transform.position, moveTo, placementVelocity / dist);

        // Orient the object.
        // We are using the return value from Physics.Raycast to instruct
        // the OrientObject function to align to the vertical surface if appropriate.
        OrientObject(hit, surfaceNormal);
    }

    /// <summary>
    /// Orients the object so that it faces the user.
    /// </summary>
    /// <param name="alignToVerticalSurface">
    /// If true and the object is to be placed on a vertical surface, 
    /// orient parallel to the target surface.  If false, orient the object 
    /// to face the user.
    /// </param>
    /// <param name="surfaceNormal">
    /// The target surface's normal vector.
    /// </param>
    /// <remarks>
    /// The aligntoVerticalSurface parameter is ignored if the object
    /// is to be placed on a horizontalSurface
    /// </remarks>
    private void OrientObject(bool alignToVerticalSurface, Vector3 surfaceNormal)
    {
        Quaternion rotation = Camera.main.transform.localRotation;

        // If the user's gaze does not intersect with the Spatial Mapping mesh,
        // orient the object towards the user.
        if (alignToVerticalSurface && (PlacementSurface == PlacementSurfaces.Vertical))
        {
            // We are placing on a vertical surface.
            // If the normal of the Spatial Mapping mesh indicates that the
            // surface is vertical, orient parallel to the surface.
            if (Mathf.Abs(surfaceNormal.y) <= (1 - upNormalThreshold))
            {
                rotation = Quaternion.LookRotation(-surfaceNormal, Vector3.up);
            }
        }
        else
        {
            rotation.x = 0f;
            rotation.z = 0f;
        }

        gameObject.transform.rotation = rotation;
    }

    /// <summary>
    /// Displays the bounds asset.
    /// </summary>
    /// <param name="canBePlaced">
    /// Specifies if the object is in a valid placement location.
    /// </param>
    private void DisplayBounds(bool canBePlaced)
    {
        // Ensure the bounds asset is sized and positioned correctly.
        boundsAsset.transform.localPosition = boxCollider.center;
        boundsAsset.transform.localScale = boxCollider.size;
        boundsAsset.transform.rotation = gameObject.transform.rotation;

        // Apply the appropriate material.
        if (canBePlaced)
        {
            boundsAsset.GetComponent<Renderer>().sharedMaterial = PlaceableBoundsMaterial;
        }
        else
        {
            boundsAsset.GetComponent<Renderer>().sharedMaterial = NotPlaceableBoundsMaterial;
        }

        // Show the bounds asset.
        boundsAsset.SetActive(true);
    }

    /// <summary>
    /// Displays the placement shadow asset.
    /// </summary>
    /// <param name="position">
    /// The position at which to place the shadow asset.
    /// </param>
    /// <param name="surfaceNormal">
    /// The normal of the surface on which the asset will be placed
    /// </param>
    /// <param name="canBePlaced">
    /// Specifies if the object is in a valid placement location.
    /// </param>
    private void DisplayShadow(Vector3 position,
                            Vector3 surfaceNormal,
                            bool canBePlaced)
    {
        // Rotate the shadow so that it is displayed on the correct surface and matches the object.
        float rotationX = 0.0f;
        if (PlacementSurface == PlacementSurfaces.Horizontal)
        {
            rotationX = 90.0f;
        }
        Quaternion rotation = Quaternion.Euler(rotationX, gameObject.transform.rotation.eulerAngles.y, 0);

        shadowAsset.transform.localScale = boxCollider.size;
        shadowAsset.transform.rotation = rotation;

        // Apply the appropriate material.
        if (canBePlaced)
        {
            shadowAsset.GetComponent<Renderer>().sharedMaterial = PlaceableShadowMaterial;
        }
        else
        {
            shadowAsset.GetComponent<Renderer>().sharedMaterial = NotPlaceableShadowMaterial;
        }

        // Show the shadow asset as appropriate.        
        if (position != Vector3.zero)
        {
            // Position the shadow a small distance from the target surface, along the normal.
            shadowAsset.transform.position = position + (0.01f * surfaceNormal);
            shadowAsset.SetActive(true);
        }
        else
        {
            shadowAsset.SetActive(false);
        }
    }

    /// <summary>
    /// Determines if two distance values should be considered equivalent. 
    /// </summary>
    /// <param name="d1">
    /// Distance to compare.
    /// </param>
    /// <param name="d2">
    /// Distance to compare.
    /// </param>
    /// <returns>
    /// True if the distances are within the desired tolerance, otherwise false.
    /// </returns>
    private bool IsEquivalentDistance(float d1, float d2)
    {
        float dist = Mathf.Abs(d1 - d2);
        return (dist <= distanceThreshold);
    }

    /// <summary>
    /// Called when the GameObject is unloaded.
    /// </summary>
    private void OnDestroy()
    {
        // Unload objects we have created.
        Destroy(boundsAsset);
        boundsAsset = null;
        Destroy(shadowAsset);
        shadowAsset = null;
    }
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,我来为您封装一个拖动LazyColumn的item可改变item位置组件。首先,我们需要添加以下依赖项: ```groovy // Jetpack Compose implementation "androidx.compose.ui:ui:$compose_version" implementation "androidx.compose.ui:ui-tooling:$compose_version" implementation "androidx.compose.foundation:foundation:$compose_version" implementation "androidx.compose.foundation:foundation-layout:$compose_version" // Drag and drop support implementation "androidx.compose.foundation:foundation-drag:$compose_version" ``` 然后,我们可以创建一个名为 `DraggableLazyColumn` 的组件,它将包含一个 `LazyColumn` 和一个 `Box`,用于渲染拖动操作。下面是完整的代码: ```kotlin import androidx.compose.foundation.background import androidx.compose.foundation.gestures.detectDragGestures import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.height import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.selection.selectable import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.MaterialTheme import androidx.compose.material.Surface import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Rect import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.layout.Layout import androidx.compose.ui.layout.Measurable import androidx.compose.ui.layout.ParentDataModifier import androidx.compose.ui.layout.Placeable import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.dp @Composable fun <T> DraggableLazyColumn( items: List<T>, itemContent: @Composable (T) -> Unit, modifier: Modifier = Modifier, itemHeight: Int = 64.dp.toInt(), onItemMoved: (fromIndex: Int, toIndex: Int) -> Unit ) { val state = rememberLazyListState() val selectedIndexes = remember { mutableStateListOf<Int>() } val dragState = rememberDragState() Box(modifier = modifier) { LazyColumn(state = state) { itemsIndexed(items) { index, item -> val isSelected = selectedIndexes.contains(index) val draggableModifier = Modifier .dragged(dragState) { if (!isSelected) { selectedIndexes.clear() selectedIndexes.add(index) } } .selectable( selected = isSelected, onClick = { select(index, isSelected) } ) DraggableItem( modifier = draggableModifier, height = itemHeight, index = index, isSelected = isSelected, onItemMoved = onItemMoved ) { itemContent(item) } } } if (selectedIndexes.isNotEmpty()) { val height = selectedIndexes.size * itemHeight val y = dragState.position.y - height / 2 Box( modifier = Modifier .height(height.dp) .background(Color.White.copy(alpha = 0.8f)) .offset(y.dp) .dragged(dragState), contentAlignment = androidx.compose.ui.Alignment.Center ) { selectedIndexes.forEach { index -> DraggableItem( modifier = Modifier.selectable(selected = true) { select(index, true) }, height = itemHeight, index = index, isSelected = true, onItemMoved = onItemMoved ) { itemContent(items[index]) } } } } } } @Composable fun DraggableItem( modifier: Modifier, height: Int, index: Int, isSelected: Boolean, onItemMoved: (fromIndex: Int, toIndex: Int) -> Unit, content: @Composable () -> Unit ) { Surface( modifier = modifier, elevation = 8.dp, shape = RoundedCornerShape(8.dp), color = if (isSelected) MaterialTheme.colors.secondary else MaterialTheme.colors.surface ) { Layout( content = content, modifier = Modifier.height(height.dp), measurePolicy = { measurables, constraints -> val placeables = measurables.map { measurable -> measurable.measure(constraints) } layout(constraints.maxWidth, height) { placeables.forEachIndexed { index, placeable -> val parentData = placeable.parentData as DraggableItemParentData parentData.measurable = measurables[index] parentData.placeable = placeable } } } ) { measurables, constraints -> val placeables = measurables.map { measurable -> val parentData = measurable.parentData as DraggableItemParentData parentData.measurable = measurable parentData.placeable!! } layout(constraints.maxWidth, height) { placeables.forEach { placeable -> placeable.placeRelative(0, 0) } } } } } private class DraggableItemParentData(var measurable: Measurable? = null, var placeable: Placeable? = null) : ParentDataModifier { override fun Density.modifyParentData(parentData: Any?): Any { return DraggableItemParentData() } } @Composable private fun Modifier.dragged(dragState: DragState, onDrag: () -> Unit = {}): Modifier { return pointerInput(Unit) { detectTapGestures(onLongPress = { dragState.onLongPress() }) detectDragGestures( onDragStart = { dragState.onStart(it) }, onDrag = { dragState.onDrag(it); onDrag() }, onDragEnd = { dragState.onEnd() } ) } } private fun Modifier.selectable(selected: Boolean, onClick: () -> Unit): Modifier { return if (selected) { this.background(Color.LightGray) } else { this }.pointerInput(Unit) { detectTapGestures(onDoubleTap = onClick) } } private class DragState { var isDragging by mutableStateOf(false) var position by mutableStateOf(Offset.Zero) var offset by mutableStateOf(Offset.Zero) var index by mutableStateOf(-1) fun onLongPress() { isDragging = true } fun onStart(offset: Offset) { this.offset = offset } fun onDrag(offset: Offset) { this.position = offset - this.offset } fun onEnd() { isDragging = false } } @Composable private fun rememberDragState(): DragState { return remember { DragState() } } @Composable private fun LayoutScope.itemsIndexed( items: List<Any>, itemContent: @Composable (Int, Any) -> Unit ) { items.forEachIndexed { index, item -> itemContent(index, item) } } private fun Modifier.offset(y: Int): Modifier { return this.then(Modifier.offset { IntOffset(0, y) }) } private fun select(index: Int, isSelected: Boolean) { if (isSelected) { selectedIndexes.remove(index) } else { selectedIndexes.add(index) } } private var selectedIndexes = mutableStateListOf<Int>() ``` 在上述代码中,我们使用 `LazyColumn` 显示所有的项目。每个项目都被包装在 `DraggableItem` 组件中,该组件可以响应拖动手势。我们还创建了一个 `DragState` 类来跟踪拖动操作的状态。 当用户长按某个项目时,我们将启动拖动状态,并将该项目添加到选定的项目列表中。拖动状态时,我们将在拖动操作的位置显示一个 `Box`,其中包含所有选定的项目。 当用户松开鼠标按钮时,我们会在列表中更新项目的顺序,并将拖动操作状态重置为初始状态。 现在,我们可以在调用 `DraggableLazyColumn` 时将项目列表作为参数传递。我们还需要提供一个 `itemContent` 函数,该函数接受单个项目并呈现其内容。我们还可以指定项目的默认高度,以及在移动项目时调用的回调函数。 下面是一个使用示例: ```kotlin val items = List(20) { "Item $it" } DraggableLazyColumn( items = items, itemContent = { item -> Text(text = item) }, itemHeight = 64, onItemMoved = { fromIndex, toIndex -> val movedItem = items.removeAt(fromIndex) items.add(toIndex, movedItem) } ) ``` 这个例子将呈现一个包含 20 个项目的列表,每个项目的高度为 64dp。当用户长按某个项目并开始拖动时,其他选定的项目将显示在拖动操作的下方。当用户松开鼠标按钮时,项目列表的顺序将更新。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值