Я видел несколько постов здесь, спрашивающих об этом, и ответ, кажется, что вы по своему усмотрению. Я думаю, что взломал это достаточно просто, поэтому я хотел бы поделиться своим подходом для комментариев и улучшений.
На высоком уровне мой подход такой:
- Выберите одну или несколько строк обычным способом
- Показать маркер перетаскивания только в первой выбранной строке, и только если все выбранные строки являются смежными.
- Когда начинается перетаскивание, сверните высоты всех других выбранных строк, кроме первой. Так что теперь мы просто перетаскиваем одну строку вокруг.
- Остановим другие выбранные строки, становящиеся строкой назначения
- Когда перетаскивание закончится, восстановите другие выбранные строки и расположите их в таблице
Другие вещи, о которых я подумал или попробовал:
- Не скрывать другие выбранные строки. Я запутался в том, как плавно перемещать их, и не смог заставить его работать
- Если есть несколько блоков выбранных строк, тогда я не знаю, какое поведение ожидает пользователь
Для расширения с помощью нескольких фрагментов кода:
Примечание: vList - это мой UITableView
2. Показывать маркер перетаскивания только в первой выбранной строке и только в том случае, если все выбранные строки являются смежными
В Java (так как я делаю MVP, и этот Presenter используется совместно с Android) :
if (isContiguousSelection(selectedRows)) {
view.setDragHandleToRow(selectedRows[0]);
} else {
view.setDragHandleToRow(null);
}
boolean isContiguousSelection (int[] selectedRows) {
if (selectedRows.length == 0) {
return false;
}
if (selectedRows.length == 1) {
return true;
}
for (int i=0; i<selectedRows.length-1; i++) {
// Check the n+1th element is one more than the nth element
if (selectedRows[i+1] != selectedRows[i]+1) {
return false;
}
}
return true;
}
В ViewController:
func tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath) -> Bool {
return indexPath == dragHandleRowCurrent
}
Теперь нам нужно вызвать canMoveRowAt. Обычно он вызывается при перезагрузке данных, но мы не хотим перезагружать весь набор данных без необходимости. Итак:
var dragHandleRowCurrent: IndexPath? = nil
func setDragHandle(to row: IndexPath?) {
// Save selections; reload individual rows to reset handles; restore selections
let savedSelections = saveSelections()
if let dragHandleRowCurrent = dragHandleRowCurrent {
// Remove dragHandle from where it previously was
vList.reloadRows(at: [dragHandleRowCurrent], with: .automatic)
}
if let row = row {
// Add dragHandle to row
vList.reloadRows(at: [row], with: .automatic)
}
dragHandleRowCurrent = row
restoreSelections(selections: savedSelections)
}
func saveSelections() -> [IndexPath] {
return vList.indexPathsForSelectedRows ?? []
}
func restoreSelections(selections: [IndexPath]) {
for indexPath in selections {
vList.selectRow(at: indexPath, animated: false, scrollPosition: .none)
}
}
Итак, теперь у нас есть ручка перетаскивания только в первой выбранной строке. Ура.
3. Когда начинается перетаскивание, сверните все остальные выбранные строки, кроме первых
Это кажется довольно простым, но есть несколько ошибок.
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
// Collapse height if row is selected while moving
if vList.indexPathsForSelectedRows?.contains(indexPath) == true
&& activeMoveSourceIndexPath != nil {
// This height leaves a little bit of visible row.
// Move to top of list seems not to work when it is zero
// But this also good as it leaves a marker for the collapsed rows when dragging
return 1
} else {
return UITableView.automaticDimension
}
}
Обратите внимание, что высота установлена на 1, а не на 0, так как я не мог заставить его правильно перетаскиваться с нуля.
Но. Мы должны вызвать высоту ForRowAt. Обычно это вызывается, когда данные перезагружаются, но мы хотим запустить его, когда начинается перетаскивание. Если мы перезагрузим данные, то жест перетаскивания будет прекращен. Я нашел ответ здесь на SO: если вы вызываете beginUpdates (), а затем endUpdates (), между которыми ничего нет, то запускается heightForRowAt. Странно, но это правда. Я не знаю, как fr agile, но в данный момент все работает нормально.
Это приводит к:
var activeMoveSourceIndexPath: IndexPath? = nil // sourceIndexPath if we are in the middle of a move
func tableView(_ tableView: UITableView, targetIndexPathForMoveFromRowAt sourceIndexPath: IndexPath, toProposedIndexPath proposedDestinationIndexPath: IndexPath) -> IndexPath {
<snip snip>
// If start of a move, hide selected rows other than the source row
if activeMoveSourceIndexPath == nil {
activeMoveSourceIndexPath = sourceIndexPath
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.01) {
self.vList.beginUpdates() // beginUpdates then endUpdates triggers heightForRowAt without reloading data
self.vList.endUpdates()
}
<snip snip>
}
Я сделал это асин c В противном случае это, казалось, мешало жесту перетаскивания.
4. Остановите другие выбранные строки, становясь строкой назначения
Это было более рискованно, чем я ожидал. Нам нужно знать направление, в котором в данный момент движется перетаскивание, чтобы мы могли пропустить другие выбранные строки в правильном направлении (вверх или вниз). Любые предложения по его оптимизации?
Вот полная версия targetIndexPathForMoveFromRowAt, включая код в 3. выше:
var activeMoveSourceIndexPath: IndexPath? = nil // sourceIndexPath if we are in the middle of a drag
var activeMoveLastDragIndexPath: IndexPath! = nil // Last known drag position. Used to determine drag direction
var activeMoveLastDragDirectionDown = true // Last known drag direction
func tableView(_ tableView: UITableView, targetIndexPathForMoveFromRowAt sourceIndexPath: IndexPath, toProposedIndexPath proposedDestinationIndexPath: IndexPath) -> IndexPath {
let cntRows = self.tableView(vList, numberOfRowsInSection: 0)
let cntSelected = vList.indexPathsForSelectedRows?.count ?? 1 // Should never be nil
// Determine drag direction
activeMoveLastDragIndexPath = activeMoveLastDragIndexPath ?? sourceIndexPath
if proposedDestinationIndexPath > activeMoveLastDragIndexPath {
activeMoveLastDragDirectionDown = true
} else if proposedDestinationIndexPath < activeMoveLastDragIndexPath {
activeMoveLastDragDirectionDown = false
} else {
// Leave unchanged if equal
}
activeMoveLastDragIndexPath = proposedDestinationIndexPath
// When moving multiple rows, skip over those rows as destinations
var destIndexPath: IndexPath!
if proposedDestinationIndexPath > sourceIndexPath
&& proposedDestinationIndexPath < IndexPath(row: sourceIndexPath.row + cntSelected) {
// Proposed destination is one of the selected rows
if activeMoveLastDragDirectionDown {
// Moving down to one of the selected rows
if sourceIndexPath.row + cntSelected == cntRows {
// Selected rows are at the end of the list, can't move after there
destIndexPath = sourceIndexPath
} else {
destIndexPath = IndexPath(row: sourceIndexPath.row + cntSelected)
}
} else {
// Moving up to one of the selected rows
destIndexPath = sourceIndexPath
}
} else {
// Proposed destination is not one of the selected rows
destIndexPath = proposedDestinationIndexPath
}
// If start of a move, collape the selected rows other than the source row
if activeMoveSourceIndexPath == nil {
self.activeMoveSourceIndexPath = sourceIndexPath
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.01) {
self.vList.beginUpdates() // beginUpdates then endUpdates triggers heightForRowAt without reloading data
self.vList.endUpdates()
}
}
return destIndexPath
}
5. Когда перетаскивание закончится, восстановите остальные выбранные строки и расположите их в таблице
func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
activeMoveSourceIndexPath = nil
activeMoveLastDragIndexPath = nil
if sourceIndexPath == destinationIndexPath {
// Not moving anywhere. Restore the selected rows and exit
vList.beginUpdates() // beginUpdates-endUpdates triggers heightForRowAt without reloading data
vList.endUpdates()
return
}
// Rearrange the other selected rows in the table
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.01) {
self.vList.beginUpdates()
let selectedIndexPaths = self.vList.indexPathsForSelectedRows!
if sourceIndexPath > destinationIndexPath {
// Moving up, move each row into position from first to last
var i = 0
for selectedIndexPath in selectedIndexPaths.sorted(by: { $0.row < $1.row }) { // Ascending
if selectedIndexPath == destinationIndexPath {
// Source row will have already been moved by framework
continue
}
self.vList.moveRow(at: IndexPath(row: selectedIndexPath.row), to: IndexPath(row: destinationIndexPath.row + 1 + i))
i = i + 1
}
} else {
// Moving down, move each row into position from last to first
var i = 0
for selectedIndexPath in selectedIndexPaths.sorted(by: { $0.row > $1.row }) { // Descending
if selectedIndexPath == destinationIndexPath {
// Source row will have already been moved by framework
continue
}
self.vList.moveRow(at: IndexPath(row: selectedIndexPath.row), to: IndexPath(row: destinationIndexPath.row - i))
i = i + 1
}
}
self.vList.endUpdates()
// TODO Update the data source
}
}
Итак, мы go. Кажется, работает нормально (с одной ошибкой, см. Ниже). Но я уверен, что вы умные люди можете улучшить это. Давайте ваши мысли!
PS Ошибка. По какой-то причине, если непрерывный блок выбранных строк включает в себя строки со второй по последнюю, последняя (невыбранная) строка представляется скрытой. Все работает хорошо, хотя. Я еще не выследил это.