#include "common.h" #include #include "gui/materialDesign.h" #include "gui/input/InputProvider.h" #include "RecyclerView.h" RecyclerView::RecyclerView(int x, int y, int width, int height, Mode mode) : _width(width), _height(height), _mode(mode), _rows(0), _columns(0) , _viewPoolFreeCount(0), _viewPoolTotalCount(0) , _xOffset(0), _yOffset(0), _xPadding(0), _yPadding(0) , _xSpacing(0), _ySpacing(0), _itemWidth(0), _itemHeight(0) , _itemCount(0), _selectedItem(nullptr) , _curRangeStart(0), _curRangeLength(0) { _position.x = x; _position.y = y; } RecyclerView::~RecyclerView() { if (_adapter) { for (u32 i = 0; i < _viewPoolTotalCount; i++) { _adapter->DestroyView(_viewPool[i].view); } } } void RecyclerView::SetAdapter(const RecyclerAdapter* adapter, int initialSelectedIndex) { if (_adapter) { _selectedItem = nullptr; for (u32 i = 0; i < _viewPoolTotalCount; i++) { _adapter->DestroyView(_viewPool[i].view); } _viewPool.reset(); _viewPoolFreeCount = 0; _viewPoolTotalCount = 0; } _adapter = adapter; _adapter->GetViewSize(_itemWidth, _itemHeight); _itemCount = _adapter->GetItemCount(); if (_mode == Mode::HorizontalList || _mode == Mode::HorizontalGrid) { if (_mode == Mode::HorizontalList) { _rows = 1; } else { _rows = std::max(1, _height / _itemHeight); } _columns = (_width + _xSpacing + _itemWidth - 1) / (_xSpacing + _itemWidth) + 1; _viewPoolTotalCount = _rows * (_columns + 1) + 1; } else { if (_mode == Mode::VerticalList) { _columns = 1; } else { _columns = std::max(1, _width / _itemWidth); } _rows = (_height + _ySpacing + _itemHeight - 1) / (_ySpacing + _itemHeight) + 1; _viewPoolTotalCount = (_rows + 1) * _columns + 1; } LOG_DEBUG("_rows: %d\n", _rows); LOG_DEBUG("_columns: %d\n", _columns); LOG_DEBUG("_viewPoolTotalCount: %d\n", _viewPoolTotalCount); _viewPool = std::unique_ptr(new ViewPoolEntry[_viewPoolTotalCount]); for (u32 i = 0; i < _viewPoolTotalCount; i++) { _viewPool[i].view = _adapter->CreateView(); _viewPool[i].view->SetParent(this); _viewPool[i].itemIdx = -1; } _viewPoolFreeCount = _viewPoolTotalCount; if (initialSelectedIndex < 0 || initialSelectedIndex >= (int)_itemCount) { initialSelectedIndex = 0; } EnsureVisible(initialSelectedIndex, false); if (_itemCount > 0) { SetSelectedItem(initialSelectedIndex); } } void RecyclerView::InitVram(const VramContext& vramContext) { for (u32 i = 0; i < _viewPoolTotalCount; i++) { _viewPool[i].view->InitVram(vramContext); } } void RecyclerView::Update() { if (_itemCount == 0) { return; } if (!_scrollOffsetAnimator.IsFinished()) { _scrollOffsetAnimator.Update(); } int rangeStartIndex = 0; int rangeEndIndex = 0; if (_mode == Mode::HorizontalList || _mode == Mode::HorizontalGrid) { _xOffset = _scrollOffsetAnimator.GetValue(); rangeStartIndex = ((-_xOffset - _xPadding) / (_xSpacing + _itemWidth) - 1) * _rows; rangeEndIndex = rangeStartIndex + (_columns + 1) * _rows; } else { _yOffset = _scrollOffsetAnimator.GetValue(); rangeStartIndex = ((-_yOffset - _yPadding) / (_ySpacing + _itemHeight) - 1) * _columns; rangeEndIndex = rangeStartIndex + (_rows + 1) * _columns; } rangeStartIndex = std::clamp(rangeStartIndex, 0, (int)_itemCount - 1); rangeEndIndex = std::clamp(rangeEndIndex, 0, (int)_itemCount); if (_curRangeStart != rangeStartIndex || _curRangeLength != rangeEndIndex - rangeStartIndex) { LOG_DEBUG("range: %d - %d\n", rangeStartIndex, rangeEndIndex - 1); if (_curRangeLength != 0) { if (_curRangeStart < rangeStartIndex) { ReleaseRange(_curRangeStart, rangeStartIndex); } if (rangeEndIndex < _curRangeStart + _curRangeLength) { ReleaseRange(rangeEndIndex, _curRangeStart + _curRangeLength); } } BindRange(rangeStartIndex, rangeEndIndex); _curRangeStart = rangeStartIndex; _curRangeLength = rangeEndIndex - rangeStartIndex; } for (u32 i = _viewPoolFreeCount; i < _viewPoolTotalCount; i++) { UpdatePosition(_viewPool[i]); _viewPool[i].view->Update(); } } void RecyclerView::Draw(GraphicsContext& graphicsContext) { for (u32 i = _viewPoolFreeCount; i < _viewPoolTotalCount; i++) { _viewPool[i].view->Draw(graphicsContext); } } void RecyclerView::VBlank() { for (u32 i = _viewPoolFreeCount; i < _viewPoolTotalCount; i++) { _viewPool[i].view->VBlank(); } } View* RecyclerView::MoveFocus(View* currentFocus, FocusMoveDirection direction, View* source) { if (_itemCount == 0) { return View::MoveFocus(currentFocus, direction, this); } if (_mode == Mode::HorizontalList || _mode == Mode::HorizontalGrid) { return MoveFocusHorizontal(currentFocus, direction, source); } else { return MoveFocusVertical(currentFocus, direction, source); } } View* RecyclerView::MoveFocusHorizontal(View* currentFocus, FocusMoveDirection direction, View* source) { if (!_selectedItem || currentFocus != _selectedItem->view) { // incoming focus if (direction != FocusMoveDirection::Down) { return nullptr; } int idx = (-_xOffset + currentFocus->GetPosition().x - _xPadding + ((_xSpacing + _itemWidth) >> 1)) / (_xSpacing + _itemWidth) * _rows; SetSelectedItem(std::clamp(idx, 0, ((int)_itemCount - 1) / _rows * _rows)); return _selectedItem != nullptr ? _selectedItem->view : this; } int row = _selectedItem->itemIdx % _rows; if ((row == 0 && direction == FocusMoveDirection::Up) || (row == _rows - 1 && direction == FocusMoveDirection::Down) || (_selectedItem->itemIdx < _rows && direction == FocusMoveDirection::Left) || (_selectedItem->itemIdx / _rows >= (int)(_itemCount - 1) / _rows && direction == FocusMoveDirection::Right)) { return View::MoveFocus(currentFocus, direction, this); } if (direction == FocusMoveDirection::Left) { int idx = _selectedItem->itemIdx; if (idx - _rows >= 0) { idx -= _rows; } SetSelectedItem(idx); } else if (direction == FocusMoveDirection::Right) { int idx = _selectedItem->itemIdx + _rows; idx = std::min(idx, (int)_itemCount - 1); SetSelectedItem(idx); } else if (direction == FocusMoveDirection::Up) { int idx = (_selectedItem->itemIdx / _rows * _rows) + std::clamp((_selectedItem->itemIdx % _rows) - 1, 0, _rows - 1); SetSelectedItem(std::clamp(idx, 0, (int)_itemCount - 1)); } else if (direction == FocusMoveDirection::Down) { int idx = (_selectedItem->itemIdx / _rows * _rows) + std::clamp((_selectedItem->itemIdx % _rows) + 1, 0, _rows - 1); SetSelectedItem(std::clamp(idx, 0, (int)_itemCount - 1)); } return _selectedItem != nullptr ? _selectedItem->view : this; } View* RecyclerView::MoveFocusVertical(View* currentFocus, FocusMoveDirection direction, View* source) { if (!_selectedItem || currentFocus != _selectedItem->view) { // incoming focus if (direction != FocusMoveDirection::Right) { return nullptr; } int idx = (-_yOffset + currentFocus->GetPosition().y - _yPadding + ((_ySpacing + _itemHeight) >> 1)) / (_ySpacing + _itemHeight) * _columns; SetSelectedItem(std::clamp(idx, 0, ((int)_itemCount - 1) / _columns * _columns)); return _selectedItem != nullptr ? _selectedItem->view : this; } int column = _selectedItem->itemIdx % _columns; if ((column == 0 && direction == FocusMoveDirection::Left) || (column == _columns - 1 && direction == FocusMoveDirection::Right) || (_selectedItem->itemIdx < _columns && direction == FocusMoveDirection::Up) || (_selectedItem->itemIdx / _columns >= (int)(_itemCount - 1) / _columns && direction == FocusMoveDirection::Down)) { return View::MoveFocus(currentFocus, direction, this); } if (direction == FocusMoveDirection::Up) { int idx = _selectedItem->itemIdx; if (idx - _columns >= 0) { idx -= _columns; } SetSelectedItem(idx); } else if (direction == FocusMoveDirection::Down) { int idx = _selectedItem->itemIdx + _columns; idx = std::min(idx, (int)_itemCount - 1); SetSelectedItem(idx); } else if (direction == FocusMoveDirection::Left) { int idx = (_selectedItem->itemIdx / _columns * _columns) + std::clamp((_selectedItem->itemIdx % _columns) - 1, 0, _columns - 1); SetSelectedItem(std::clamp(idx, 0, (int)_itemCount - 1)); } else if (direction == FocusMoveDirection::Right) { int idx = (_selectedItem->itemIdx / _columns * _columns) + std::clamp((_selectedItem->itemIdx % _columns) + 1, 0, _columns - 1); SetSelectedItem(std::clamp(idx, 0, (int)_itemCount - 1)); } return _selectedItem != nullptr ? _selectedItem->view : this; } bool RecyclerView::HandleInput(const InputProvider& inputProvider, FocusManager& focusManager) { if (inputProvider.Triggered(InputKey::L | InputKey::R)) { int direction = inputProvider.Triggered(InputKey::L) ? 1 : -1; int selected = _selectedItem->itemIdx; if (_mode == Mode::HorizontalList || _mode == Mode::HorizontalGrid) { int visibleColumns = _width / (_itemWidth + _xSpacing); SetScrollOffset(_scrollOffsetAnimator.GetTargetValue() + direction * visibleColumns * (_itemWidth + _xSpacing), true); int row = selected % _rows; selected = std::clamp(selected - direction * visibleColumns * _rows, 0, (int)_itemCount - 1); selected = selected / _rows * _rows + row; // try to stay in the same row selected = std::clamp(selected, 0, (int)_itemCount - 1); // but clamp to the last item } else { int visibleRows = _height / (_itemHeight + _ySpacing); SetScrollOffset(_scrollOffsetAnimator.GetTargetValue() + direction * visibleRows * (_itemHeight + _ySpacing), true); int column = selected % _columns; selected = std::clamp(selected - direction * visibleRows * _columns, 0, (int)_itemCount - 1); selected = selected / _columns * _columns + column; // try to stay in the same column selected = std::clamp(selected, 0, (int)_itemCount - 1); // but clamp to the last item } focusManager.Unfocus(); SetSelectedItem(selected); focusManager.Focus(_selectedItem->view); return true; } return View::HandleInput(inputProvider, focusManager); } Point RecyclerView::GetItemPosition(int itemIdx) { int x = 0; int y = 0; switch (_mode) { case Mode::HorizontalList: { x = _xPadding + itemIdx * (_xSpacing + _itemWidth); y = _yPadding; break; } case Mode::HorizontalGrid: { x = _xPadding + (itemIdx / _rows) * (_xSpacing + _itemWidth); y = _yPadding + (itemIdx % _rows) * (_ySpacing + _itemHeight); break; } case Mode::VerticalList: { x = _xPadding; y = _yPadding + itemIdx * (_ySpacing + _itemHeight); break; } case Mode::VerticalGrid: { x = _xPadding + (itemIdx % _columns) * (_xSpacing + _itemWidth); y = _yPadding + (itemIdx / _columns) * (_ySpacing + _itemHeight); break; } } return Point(x, y); } void RecyclerView::UpdatePosition(ViewPoolEntry& viewPoolEntry) { auto itemPosition = GetItemPosition(viewPoolEntry.itemIdx); viewPoolEntry.view->SetPosition( _position.x + _xOffset + itemPosition.x, _position.y + _yOffset + itemPosition.y); } RecyclerView::ViewPoolEntry* RecyclerView::GetViewPoolEntryByItemIndex(int itemIdx) { for (u32 i = _viewPoolFreeCount; i < _viewPoolTotalCount; i++) { if (_viewPool[i].itemIdx == (int)itemIdx) { return &_viewPool[i]; } } return nullptr; } RecyclerView::ViewPoolEntry* RecyclerView::BindViewPoolEntry(int itemIdx) { if (_viewPoolFreeCount == 0) { LOG_FATAL("No free view pool entries left\n"); while (true); return nullptr; } auto& entry = _viewPool[_viewPoolFreeCount - 1]; _viewPoolFreeCount--; entry.itemIdx = itemIdx; _adapter->BindView(entry.view, itemIdx); return &entry; } void RecyclerView::BindRange(int start, int end) { for (int i = start; i < end; i++) { if ((_selectedItem && _selectedItem->itemIdx == i) || (_curRangeLength != 0 && _curRangeStart <= i && i < _curRangeStart + _curRangeLength)) { continue; } BindViewPoolEntry(i); } } void RecyclerView::ReleaseViewPoolEntry(int itemIdx) { for (u32 i = _viewPoolFreeCount; i < _viewPoolTotalCount; i++) { if (_viewPool[i].itemIdx == (int)itemIdx) { _adapter->ReleaseView(_viewPool[i].view, _viewPool[i].itemIdx); _viewPool[i].itemIdx = -1; std::swap(_viewPool[i], _viewPool[_viewPoolFreeCount]); if (_selectedItem == &_viewPool[_viewPoolFreeCount]) { _selectedItem = &_viewPool[i]; } _viewPoolFreeCount++; return; } } } void RecyclerView::ReleaseRange(int start, int end) { for (int i = start; i < end; i++) { if (_selectedItem && _selectedItem->itemIdx == i) { continue; } ReleaseViewPoolEntry(i); } } void RecyclerView::SetSelectedItem(int itemIdx) { if (_selectedItem) { if (_selectedItem->itemIdx == itemIdx) { return; } if (_selectedItem->itemIdx < _curRangeStart || _selectedItem->itemIdx >= _curRangeStart + _curRangeLength) { ReleaseViewPoolEntry(_selectedItem->itemIdx); } _selectedItem = nullptr; } if (itemIdx < 0 || itemIdx >= (int)_itemCount) { return; } if (itemIdx >= _curRangeStart && itemIdx < _curRangeStart + _curRangeLength) { _selectedItem = GetViewPoolEntryByItemIndex(itemIdx); } else { _selectedItem = BindViewPoolEntry(itemIdx); } EnsureVisible(itemIdx, true); } int RecyclerView::GetMaxScrollOffset() { if (_mode == Mode::HorizontalGrid || _mode == Mode::HorizontalList) { int totalColumns = (_itemCount + _rows - 1) / _rows; int contentWidth = totalColumns * _itemWidth + (totalColumns - 1) * _xSpacing + _xPadding * 2; return std::min(0, _width - contentWidth); } else { int totalRows = ((_itemCount + _columns - 1) / _columns); int contentHeight = totalRows * _itemHeight + (totalRows - 1) * _ySpacing + _yPadding * 2; return std::min(0, _height - contentHeight); } } void RecyclerView::SetScrollOffset(int offset, bool animate) { offset = std::clamp(offset, GetMaxScrollOffset(), 0); if (!animate) { _scrollOffsetAnimator = Animator(offset); } else { if (std::abs(offset - _scrollOffsetAnimator.GetTargetValue()) <= 128) { _scrollOffsetAnimator.Goto(offset, md::sys::motion::duration::medium1, &md::sys::motion::easing::emphasized); } else { _scrollOffsetAnimator.Goto(offset, md::sys::motion::duration::long2, &md::sys::motion::easing::standard); } } } void RecyclerView::EnsureVisible(int itemIdx, bool animate) { const auto itemPosition = GetItemPosition(itemIdx); int minItemScrollOffset; int maxItemScollOffset; if (_mode == Mode::HorizontalGrid || _mode == Mode::HorizontalList) { minItemScrollOffset = -itemPosition.x + _xPadding; maxItemScollOffset = -itemPosition.x + _width - _itemWidth - _xPadding; } else { minItemScrollOffset = -itemPosition.y + _yPadding; maxItemScollOffset = -itemPosition.y + _height - _itemHeight - _yPadding; } int targetScrollOffset = std::clamp(_scrollOffsetAnimator.GetTargetValue(), minItemScrollOffset, maxItemScollOffset); if (targetScrollOffset != _scrollOffsetAnimator.GetTargetValue()) { SetScrollOffset(targetScrollOffset, animate); } }