CollectionView中的自定义单元格重新排序行为

CollectionView中的自定义单元格重新排序行为

本文介绍了CollectionView中的自定义单元格重新排序行为的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我可以像这样重新排序我的collectionView:

I am able to reorder my collectionView like so:

但是,我希望不是所有单元都水平移动,而是要交换以下行为(即,减少单元的改组):

However, instead of all cells shifting horizontally, I would just like to swap with the following behavior (i.e. with less shuffling of cells):

我一直在使用以下委托方法:

I have been playing with the following delegate method:

func collectionView(_ collectionView: UICollectionView, targetIndexPathForMoveFromItemAt originalIndexPath: IndexPath, toProposedIndexPath proposedIndexPath: IndexPath) -> IndexPath

但是,我不确定如何实现自定义重新排序行为.

however, I am unsure how I can achieve custom reordering behavior.

推荐答案

我设法通过创建UICollectionView的子类并将交互处理添加自定义处理来实现此目的.在查看有关如何解决问题的可能提示时,我发现了本教程: http://nshint.io/blog/2015/07/16/uicollectionviews-now-have-easy-reordering/.最重要的部分是,不仅可以在UICollectionViewController上进行交互式重新排序.相关代码如下:

I managed to achieve this by creating a subclass of UICollectionView and adding custom handling to interactive movement. While looking at possible hints on how to solve your issue, I've found this tutorial : http://nshint.io/blog/2015/07/16/uicollectionviews-now-have-easy-reordering/.The most important part there was that interactive reordering can be done not only on UICollectionViewController. The relevant code looks like this :

var longPressGesture : UILongPressGestureRecognizer!

override func viewDidLoad() {
    super.viewDidLoad()

    // rest of setup

    longPressGesture = UILongPressGestureRecognizer(target: self, action: #selector(ViewController.handleLongGesture(_:)))
    self.collectionView?.addGestureRecognizer(longPressGesture)

}

func handleLongGesture(gesture: UILongPressGestureRecognizer) {

    switch(gesture.state) {

    case UIGestureRecognizerState.Began:
        guard let selectedIndexPath = self.collectionView?.indexPathForItemAtPoint(gesture.locationInView(self.collectionView)) else {
            break
        }
        collectionView?.beginInteractiveMovementForItemAtIndexPath(selectedIndexPath)
    case UIGestureRecognizerState.Changed:
        collectionView?.updateInteractiveMovementTargetPosition(gesture.locationInView(gesture.view!))
    case UIGestureRecognizerState.Ended:
        collectionView?.endInteractiveMovement()
    default:
        collectionView?.cancelInteractiveMovement()
    }
}

这需要在放置集合视图的视图控制器内部.我不知道这是否可以与UICollectionViewController一起使用,可能需要进行一些其他修改.导致我对UICollectionView进行子类化的原因是认识到,所有其他相关的类/委托方法仅被告知第一个和最后一个索引路径(即源和目标),而没有关于所有其他经过重新排列的单元格的信息,因此,它必须在核心位置停止.

This needs to be inside your view controller in which your collection view is placed. I don't know if this will work with UICollectionViewController, some additional tinkering may be needed. What led me to subclassing UICollectionView was realisation that all other related classes/delegate methods are informed only about the first and last index paths (i.e. the source and destination), and there is no information about all the other cells that got rearranged, so It needed to be stopped at the core.

SwappingCollectionView.swift :

import UIKit

extension UIView {
    func snapshot() -> UIImage {
        UIGraphicsBeginImageContext(self.bounds.size)
        self.layer.renderInContext(UIGraphicsGetCurrentContext()!)
        let image = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()
        return image
    }
}

extension CGPoint {
    func distanceToPoint(p:CGPoint) -> CGFloat {
        return sqrt(pow((p.x - x), 2) + pow((p.y - y), 2))
    }
}

struct SwapDescription : Hashable {
    var firstItem : Int
    var secondItem : Int

    var hashValue: Int {
        get {
            return (firstItem * 10) + secondItem
        }
    }
}

func ==(lhs: SwapDescription, rhs: SwapDescription) -> Bool {
    return lhs.firstItem == rhs.firstItem && lhs.secondItem == rhs.secondItem
}

class SwappingCollectionView: UICollectionView {

    var interactiveIndexPath : NSIndexPath?
    var interactiveView : UIView?
    var interactiveCell : UICollectionViewCell?
    var swapSet : Set<SwapDescription> = Set()
    var previousPoint : CGPoint?

    static let distanceDelta:CGFloat = 2 // adjust as needed

    override func beginInteractiveMovementForItemAtIndexPath(indexPath: NSIndexPath) -> Bool {

        self.interactiveIndexPath = indexPath

        self.interactiveCell = self.cellForItemAtIndexPath(indexPath)

        self.interactiveView = UIImageView(image: self.interactiveCell?.snapshot())
        self.interactiveView?.frame = self.interactiveCell!.frame

        self.addSubview(self.interactiveView!)
        self.bringSubviewToFront(self.interactiveView!)

        self.interactiveCell?.hidden = true

        return true
    }

    override func updateInteractiveMovementTargetPosition(targetPosition: CGPoint) {

        if (self.shouldSwap(targetPosition)) {

            if let hoverIndexPath = self.indexPathForItemAtPoint(targetPosition), let interactiveIndexPath = self.interactiveIndexPath {

                let swapDescription = SwapDescription(firstItem: interactiveIndexPath.item, secondItem: hoverIndexPath.item)

                if (!self.swapSet.contains(swapDescription)) {

                    self.swapSet.insert(swapDescription)

                    self.performBatchUpdates({
                        self.moveItemAtIndexPath(interactiveIndexPath, toIndexPath: hoverIndexPath)
                        self.moveItemAtIndexPath(hoverIndexPath, toIndexPath: interactiveIndexPath)
                        }, completion: {(finished) in
                            self.swapSet.remove(swapDescription)
                            self.dataSource?.collectionView(self, moveItemAtIndexPath: interactiveIndexPath, toIndexPath: hoverIndexPath)
                            self.interactiveIndexPath = hoverIndexPath

                    })
                }
            }
        }

        self.interactiveView?.center = targetPosition
        self.previousPoint = targetPosition
    }

    override func endInteractiveMovement() {
        self.cleanup()
    }

    override func cancelInteractiveMovement() {
        self.cleanup()
    }

    func cleanup() {
        self.interactiveCell?.hidden = false
        self.interactiveView?.removeFromSuperview()
        self.interactiveView = nil
        self.interactiveCell = nil
        self.interactiveIndexPath = nil
        self.previousPoint = nil
        self.swapSet.removeAll()
    }

    func shouldSwap(newPoint: CGPoint) -> Bool {
        if let previousPoint = self.previousPoint {
            let distance = previousPoint.distanceToPoint(newPoint)
            return distance < SwappingCollectionView.distanceDelta
        }

        return false
    }
}

我确实意识到有很多事情要做,但是我希望一分钟之内一切都会变得清楚.

I do realize that there is a lot going on there, but I hope everything will be clear in a minute.

  1. 使用助手方法在UIView上进行扩展以获取单元格的快照.
  2. CGPoint上进行扩展,并使用辅助方法来计算两点之间的距离.
  3. SwapDescription辅助结构-需要使用它来防止同一对项目的多次交换,从而导致动画出现故障.它的hashValue方法可以改进,但是对于这种概念证明来说已经足够好了.
  4. beginInteractiveMovementForItemAtIndexPath(indexPath: NSIndexPath) -> Bool-移动开始时调用的方法.一切都在这里设置.我们获得单元格的快照并将其添加为子视图-该快照将是用户实际在屏幕上拖动的快照.细胞本身被隐藏了.如果从此方法返回false,则交互式运动将不会开始.
  5. updateInteractiveMovementTargetPosition(targetPosition: CGPoint)-每次用户移动后调用的方法,很多.我们检查与上一个点的距离是否足够小以交换项目-当用户在屏幕上快速拖动并且交换多个项目时,如果结果不明显,这可以防止出现问题.如果可以进行交换,我们将检查是否已经在进行交换,如果不能交换,我们将交换两个项目.
  6. endInteractiveMovement()cancelInteractiveMovement()cleanup()-运动结束后,我们需要将助手恢复为默认状态.
  7. shouldSwap(newPoint: CGPoint) -> Bool-帮助方法,检查移动是否足够小,以便我们可以交换单元格.
  1. Extension on UIView with helper method to get a snapshot of a cell.
  2. Extension on CGPoint with helper method to calculate distance between two points.
  3. SwapDescription helper structure - it is needed to prevent multiple swaps of the same pair of items, which resulted in glitchy animations. Its hashValue method could be improved, but was good enough for the sake of this proof of concept.
  4. beginInteractiveMovementForItemAtIndexPath(indexPath: NSIndexPath) -> Bool - the method called when the movement begins. Everything gets setup here. We get a snapshot of our cell and add it as a subview - this snapshot will be what the user actually drags on screen. The cell itself gets hidden. If you return false from this method, the interactive movement will not begin.
  5. updateInteractiveMovementTargetPosition(targetPosition: CGPoint) - method called after each user movement, which is a lot. We check if the distance from previous point is small enough to swap items - this prevents issue when the user would drag fast across screen and multiple items would get swapped with non-obvious results. If the swap can happen, we check if it is already happening, and if not we swap two items.
  6. endInteractiveMovement(), cancelInteractiveMovement(), cleanup() - after the movement ends, we need to restore our helpers to their default state.
  7. shouldSwap(newPoint: CGPoint) -> Bool - helper method to check if the movement was small enough so we can swap cells.

这是它给出的结果:

让我知道这是否是您需要的,和/或是否需要我澄清一些内容.

Let me know if this is what you needed and/or if you need me to clarify something.

这是一个demo项目.

这篇关于CollectionView中的自定义单元格重新排序行为的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!

08-20 16:50