Как вручную выбрать путь следующего сфокусированного индекса в представлении коллекции
Как я могу вручную выбрать следующий путь индексации для моей коллекции в tvOS?
Мой вариант использования заключается в том, что у меня есть представление коллекции с динамическими ячейками, которые прокручиваются во всех направлениях, и иногда, когда ячейка в одном разделе охватывает несколько ячеек в следующем, я хочу иметь возможность вручную решить, какая ячейка будет сфокусирована следующей.
По-видимому, поведение по умолчанию таково, что представление коллекции выберет ячейку посередине предыдущей.
Например, зеленая ячейка в данный момент находится в фокусе. Когда я двигаюсь вниз, представление коллекции хочет сфокусировать красную ячейку, но я хочу, чтобы синяя ячейка была сфокусирована следующей.
Моя первоначальная идея заключалась в том, чтобы реализовать
collectionView(_:shouldUpdateFocusIn:)
делегировать функцию и вернуть
false
+ запускать обновление фокуса, когда представление коллекции выбирает "неправильный" путь индекса.
Однако по какой-то причине
shouldUpdateFocusIn
звонят несколько раз, когда я возвращаюсь
false
от него и вызывает видимое отставание.
func collectionView(_ collectionView: UICollectionView, shouldUpdateFocusIn context: UICollectionViewFocusUpdateContext) -> Bool {
if let nextIndexPath = shouldFocusCell(context.focusHeading, (context.previouslyFocusedIndexPath, context.nextFocusedIndexPath)) {
// The collection view did not select the index path of the cell we want to focus next.
// Save the correct index path and trigger a focus update.
lastFocusChange = (lastFocusChange.next, nextIndexPath)
setNeedsFocusUpdate()
// Using updateFocusIfNeeded() results in the following warning:
// WARNING: Calling updateFocusIfNeeded while a focus update is in progress. This call will be ignored.
return false
}
return true
}
Следующей идеей было сделать то же самое в
collectionView(_:didUpdateFocusIn:with:)
, но в этом случае мы обновляем фокус только после того, как он уже переместился в "неправильную" ячейку, поэтому пользователю становится очевидно, что фокус перемещается с неправильной ячейки на правильную.
Тоже не идеально.
Я использую свой собственный подкласс
UICollectionViewLayout
и
UICollectionView
, но я не вижу ничего, что можно было бы переопределить, чтобы иметь возможность вручную решить, какой путь индекса фокусировать следующим при перемещении вверх / вниз / влево / вправо перед
shouldUpdateFocusIn
называется.
Есть ли способ добиться этого?
2 ответа
Мы можем добиться этого, используя метод протокола делегата UICollectionView, пожалуйста, найдите фрагмент кода.
класс ViewController: UIViewController {
@IBOutlet weak var sideMenuTableView: UITableView!
@IBOutlet weak var collectionView: UICollectionView!
let menueItem = ["Home", "Sports", "Movies", "Listen", "Play", "Game"]
let colors: [UIColor] = [.green, .blue, .purple, .orange, .yellow, .magenta, .brown, .black, .gray, .yellow, .green, .lightGray, .cyan, .magenta, .link, .blue, .yellow, .magenta, .brown, .black, .gray, .yellow, .green, .lightGray, .cyan, .magenta, .link, .blue]
var lastFocusedIndexPath: IndexPath?
override func viewDidLoad() {
super.viewDidLoad()
lastFocusedIndexPath = IndexPath(row: 2, section: 0)
initialConfiguration()
}
override var preferredFocusEnvironments : [UIFocusEnvironment] {
return [collectionView]
}
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
updateTableViewContentInset()
}
func updateTableViewContentInset() {
self.sideMenuTableView.contentInset = UIEdgeInsets(top: 0, left: -40, bottom: 0, right: 0)
}
func initialConfiguration() {
sideMenuTableView.register(UINib.init(nibName: "SideMenuTVCell", bundle: nil), forCellReuseIdentifier: "SideMenuTVCell")
sideMenuTableView.delegate = self
sideMenuTableView.dataSource = self
sideMenuTableView.backgroundColor = .yellow
sideMenuTableView.isHidden = false
collectionView.register(UINib.init(nibName: "ColorCVCell", bundle: nil), forCellWithReuseIdentifier: "ColorCVCell")
collectionView.delegate = self
collectionView.dataSource = self
collectionView.backgroundColor = .white
}
}
extension ViewController: UITableViewDelegate, UITableViewDataSource { func tableView(_ tableView: UITableView, раздел numberOfRowsInSection: Int) -> Int {return menueItem.count}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "SideMenuTVCell", for: indexPath) as! SideMenuTVCell
cell.configureCell(string: menueItem[indexPath.row])
return cell
}
}
extension ViewController: UICollectionViewDelegate, UICollectionViewDataSource { func collectionView (_ collectionView: UICollectionView, раздел numberOfItemsInSection: Int) -> Int {return colors.count}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "ColorCVCell", for: indexPath) as! ColorCVCell
cell.backgroundColor = colors[indexPath.row]
cell.configureCell(color: colors[indexPath.row])
return cell
}
func collectionView(_ collectionView: UICollectionView, didUpdateFocusIn context: UICollectionViewFocusUpdateContext, with coordinator: UIFocusAnimationCoordinator) {
if let previousIndexPath = context.previouslyFocusedIndexPath,
let cell = collectionView.cellForItem(at: previousIndexPath) {
cell.contentView.layer.borderWidth = 0.0
cell.contentView.layer.shadowRadius = 0.0
cell.contentView.layer.shadowOpacity = 0
}
if let indexPath = context.nextFocusedIndexPath,
let cell = collectionView.cellForItem(at: indexPath) {
cell.contentView.layer.borderWidth = 8.0
cell.contentView.layer.borderColor = UIColor.black.cgColor
cell.contentView.layer.shadowColor = UIColor.black.cgColor
cell.contentView.layer.shadowRadius = 10.0
cell.contentView.layer.shadowOpacity = 0.9
cell.contentView.layer.shadowOffset = CGSize(width: 0, height: 0)
}
if let indexPath = context.previouslyFocusedIndexPath, let cell = collectionView.cellForItem(at: indexPath) {
UIView.animate(withDuration: 0.3) { () -> Void in
cell.transform = CGAffineTransform(scaleX: 1.0, y: 1.0)
}
}
if let indexPath = context.nextFocusedIndexPath, let cell = collectionView.cellForItem(at: indexPath) {
UIView.animate(withDuration: 0.3) { () -> Void in
cell.transform = CGAffineTransform(scaleX: 1.1, y: 1.1)
}
}
}
func collectionView(_ collectionView: UICollectionView, shouldUpdateFocusIn context: UICollectionViewFocusUpdateContext) -> Bool {
if let previouslyFocusedIndexPath = context.previouslyFocusedIndexPath, let cell = collectionView.cellForItem(at: previouslyFocusedIndexPath) {
let collectionViewWidth = collectionView.frame.width
let cellWidth = cell.frame.width
let rowCount = Int(ceil(collectionViewWidth / cellWidth))
let remender = previouslyFocusedIndexPath.row % rowCount
let nextIndex = previouslyFocusedIndexPath.row - remender + rowCount
if let nextFocusedInndexPath = context.nextFocusedIndexPath {
if context.focusHeading == .down {
moveFocus(to: IndexPath(row: nextIndex, section: 0))
return true
}
}
}
return true
}
private func moveFocus(to indexPath: IndexPath) {
lastFocusedIndexPath = indexPath
print(collectionView.indexPathsForVisibleItems)
DispatchQueue.main.async {
self.setNeedsFocusUpdate()
self.updateFocusIfNeeded()
}
}
func indexPathForPreferredFocusedView(in collectionView: UICollectionView) -> IndexPath? {
return lastFocusedIndexPath
}
}
Одна из возможностей - использовать
collectionView(_ collectionView:, canFocusItemAt:)
чтобы ваш collectionView знал, может ли данный indexPath получить фокус.
Ниже вы можете найти наивную реализацию этой концепции. Вам нужно будет скорректировать математику на нем в соответствии с вашими потребностями.
func collectionView(_ collectionView: UICollectionView, canFocusItemAt indexPath: IndexPath) -> Bool {
guard let currentlyFocusedCellLayoutAttributes = collectionView.layoutAttributesForItem(at: focusedIndexPath) else { return false }
guard let cellAtGivenIndexPathLayoutAttributes = collectionView.layoutAttributesForItem(at: indexPath) else { return false }
let currentlyFocusedCellOriginX = currentlyFocusedCellLayoutAttributes.frame.origin.x
let currentlyFocusedCellOriginY = currentlyFocusedCellLayoutAttributes.frame.origin.y
let currentlyFocusedCellWidth = currentlyFocusedCellLayoutAttributes.frame.width
let cellAtGivenIndexPathOriginX = cellAtGivenIndexPathLayoutAttributes.frame.origin.x
let cellAtGivenIndexPathOriginY = cellAtGivenIndexPathLayoutAttributes.frame.origin.y
let cellAtGivenIndexPathWidth = cellAtGivenIndexPathLayoutAttributes.frame.width
let offsetX = collectionView.contentOffset.x
// Scrolling horizontally is always allowed
if currentlyFocusedCellOriginY == cellAtGivenIndexPathOriginY {
return true
}
// Scrolling vertically is only allowed to the first cell (blue cell in the screenshot)
if cellAtGivenIndexPathOriginX <= offsetX {
return true
}
return false
}