


⇒ 모든 아이템들을 미리 생성하고, 실제 필요한 범위만 보이도록 관리 (가상화 정책)
1. 갱신 필요성 및 초기화
- 이전 지오메트리를 저장하여, 다음 프레임에 변경되었는지 여부를 확인
- 아이템 데이터가 변경되었는가를 확인
2. 스크롤 목표 및 애니메이션
- 목표 오프셋 계산
- 애니메이션을 적용
3. 위젯 재생성
- ReGenerateItems 함수 호출하여 뷰포트 내부에서 보이는 위젯만 생성/업데이트 실행ㅇ
4. 시각적 요소 및 정리
- 첫 줄 오프셋 정리
- 오버 스크롤 처리
- 선택 상태를 정리 → 현재 아이템 리스트 내부에서 선택된 아이템이 없음 정리하도록 함.
- 스크롤 바 업데이트 → 현재 위치에 대한 스크롤 바 업데이트
5. 다음 프레임 예약
- 갱신 예약 → 아직 모든 업데이트가 되었지 않다면, 다음 프레임에 틱을 다시 사용하도록 예약
- 스크롤 완료 알림을 진행
// CurItem : 현재 배열로부터 넘겨받은 아이템 데이터
// ItemIndex : 리스트 뷰 상에서 해당 아이템의 인덱스 번호
// StartIndex : 시작 인덱스 번호
float GenerateWidgetForItem( const ItemType& CurItem, int32 ItemIndex, int32 StartIndex, float LayoutScaleMultiplier )
{
ensure(TListTypeTraits<ItemType>::IsPtrValid(CurItem));
// 해당 아이템에 대해 이전에 생성된 위젯이 존재한다면, 그 위젯을 찾는다.
// 내부 풀링에서 확인하는 단계
// WidgetGenerator : 데이터 아이템으로부터 위젯을 생성하는 역할을 하는 컴포넌트
// 또한, 현재 생성된 위젯들이 어떤 데이터 아이템을 표현하고 있는지의 매핑도 제공한다.
// 현재 화면에 생성되어 있는 위젯 <-> 그 위젯이 나타내는 데이터 매핑!
TSharedPtr<ITableRow> WidgetForItem = WidgetGenerator.GetWidgetForItem( CurItem );
if ( !WidgetForItem.IsValid() )
{
// 만약 존재하는 위젯을 발견하지 못했을 경우,
// 즉, 이 데이터 아이템은 이전에 화면에 보이지 않았다는 의미로, 따라서 이 아이템을 위한 새로운 위젯을 생성한다.
WidgetForItem = this->GenerateNewWidget(CurItem); // 생성된 아이템에 대한 위젯
}
// 위젯이 어떤 아이템 인덱스로부터 생성되었는지 알고 있는 것이 유용
// 이는 짝수/홀수 줄 색상 처리에 도움 (아이템의 인덱스 번호를 저장)
WidgetForItem->SetIndexInList(ItemIndex);
// 현재 아이템과 그에 연결된 위젯을 발견했음을 아이템 생성기에게 알려줌.
// 즉, WidgetGenerator 는 보이는 아이템에 대해 Widget은 매핑하는 것이기 때문에 알려줌으로써 저장
WidgetGenerator.OnItemSeen( CurItem, WidgetForItem.ToSharedRef() );
// 이 과정에서 Entry Widget의 Private_OnEntryInitialized 호출 -> OnEntryInitialized 바인딩된 모든 함수 호출
// 화면에 몇 개의 위젯이 들어갈 수 있는지 판단하기 위해,
// 각 위젯의 Desired Size에 의존한다.
const TSharedRef<SWidget> NewlyGeneratedWidget = WidgetForItem->AsWidget();
NewlyGeneratedWidget->MarkPrepassAsDirty();
NewlyGeneratedWidget->SlatePrepass(LayoutScaleMultiplier);
// 이 아이템에 대한 위젯이 이미 존재하므로,
// 해당 위젯을 패넬에 다시 추가하여 실제 UI 표시한다.
if (ItemIndex >= StartIndex)
{
// 위젯들을 아래 방향으로 생성하는 중
this->AppendWidget( WidgetForItem.ToSharedRef() );
}
else
{
// 위젯을 채워 넣는 과정, 즉, 위 방향으로 생성 중
this->InsertWidget( WidgetForItem.ToSharedRef() );
}
const bool bIsVisible = NewlyGeneratedWidget->GetVisibility().IsVisible(); //현재 새롭게 생성된 위젯이 보이는가
// 현재 생성된 위젯의 크기를 사용해서
// 또는 보이지 않는 경우에는 0 크기를 사용해서 방향에 맞는 위젯치수 정보를 만듬
FTableViewDimensions GeneratedWidgetDimensions(this->Orientation, bIsVisible ? NewlyGeneratedWidget->GetDesiredSize() : FVector2D::ZeroVector);
return GeneratedWidgetDimensions.ScrollAxis;
}
/**
* - 필요한 경우 아이템에 대한 위젯을 생성(또는 갱신)하고, 더 이상 필요하지 않은 위젯은 정리한다.
* - 현재 화면에 보여지는 위젯들의 순서를 필요에 따라 재배치한다.
*/
virtual FReGenerateResults ReGenerateItems( const FGeometry& MyGeometry ) override
{
auto DoubleFractional = [](double Value) -> double
{
// 입력된 double 값의 '소수 부분만' 분리해서 반환하는 함수
return Value - FMath::TruncToDouble(Value);
};
// 패널에 존재하는 모든 아이템들을 지운다.
// 곧 올바른 순서대로 아이템을 다시 추가할 것임.
this->ClearWidgets();
// 아이템 생성 패스(단계를 시작하고, 또한 반드시 정리까지 수행되도록 보장
// 생성 -> 정리를 한 세트로 보장되어야 함.
FGenerationPassGuard GenerationPassGuard(WidgetGenerator); //RAII Scope
// 모든 아이템들을 가지고 온다.
const TArrayView<const ItemType> Items = GetItems();
if (Items.Num() > 0)
{
// 뷰에 보이는 아이템 수(부분적으로 보이는 아이템 포함)
// 예를 들어, 2.3일 때, 5 6 7(일부분 30%) 보여야함. 이때 사용되는 의미
float ItemsInView = 0.0f;
// 지금까지 생성된 위젯들의 전체 길이
// (세로 리스트일 경우 높이, 가로 리스트일 경우에는 너비)
float LengthGeneratedSoFar = 0.0f;
// 뷰(화면 영역) 안에 들어와 있는 생성된 위젯들의 길이
// 즉, 화면에 실제로 보이는 (또는 일부라도 걸쳐 있는) Row들의 총 길이를 의미
// 만약 Rect 범위 0px ~ 300px => Row 길이 50px 때, 6개의 Row가 화면에 걸침.
// 그리고, 300px이 정확하게 계산
float ViewLengthUsedSoFar = 0.0f;
// 현재 스크롤된 위치를 기준으로,
// 생성을 시작해야 하는 아이템의 인덱스
// 최소한 1개의 아이템은 반드시 생성해야 한다.
// 현재 스크롤의 Offset(스크롤 위치)일 때, 개수 범위내 제한에서 몇번째 인덱스가 시작 인덱스인지를 확인
int32 StartIndex = FMath::Clamp( (int32)(FMath::FloorToDouble(CurrentScrollOffset)), 0, Items.Num() - 1 );
// 생성된 첫 번째 아이템의 길이
// 이 아이템은 사용자가 스크롤을 요청한 바로 그 위치에 놓이게 된다.
// 예를 들어, 스크롤 Offset값이 120px일 때 2번 인덱스의 Row위치가 100-150 사이면,
// 2번 인덱스가 첫 시작점 Offset의 Row로 인식
float FirstItemLength = 0.0f;
// 시나리오 a를 가정하는 위젯들을 생성한다
bool bHasFilledAvailableArea = false;
bool bAtEndOfList = false;
// 현재 위젯(리스트 뷰)의 지오매트리의 최종 Transform의 Scale값
// 즉, 화면에 렌더링되어지는 최종 Transform값이 나오는 것임.
const float LayoutScaleMultiplier = MyGeometry.GetAccumulatedLayoutTransform().GetScale();
// 스크롤 방향 축/뷰의 물리적크기
// 테이블(리스트) 레이아웃을 계산할 때 X축인지 Y축인지를 구1분할 필요가 없도록 추상화한 구조체
FTableViewDimensions MyDimensions(this->Orientation, MyGeometry.GetLocalSize());
// 실제 위젯이 그려지는 부분
for( int32 ItemIndex = StartIndex; !bHasFilledAvailableArea && ItemIndex < Items.Num(); ++ItemIndex )
{
const ItemType& CurItem = Items[ItemIndex];
// 현재 아이템이 유효한가, ItemType은 데이터 아이템들의 배열을 관찰
if (!TListTypeTraits<ItemType>::IsPtrValid(CurItem))
{
// Don't bother generating widgets for invalid items
continue;
}
// 현재 데이터, 현재 데이터의 리스트뷰 상 인덱스위치(0번부터 시작), 실제 시작 인덱스값, 레이아웃 스케일 크기
// 생성된 스크롤 획이 전달되어진다. 해당 아이템을 넣었을 때의 스크롤 축 방향으로 차지하는 길이 위치를 판단
const float ItemLength = GenerateWidgetForItem(CurItem, ItemIndex, StartIndex, LayoutScaleMultiplier);
const bool bIsFirstItem = ItemIndex == StartIndex;
// 시작 위치야?
if (bIsFirstItem)
{
// 첫번째 아이템을 삽입하고 나서 전달받은 스크롤 축 방향의 길이를 저장
FirstItemLength = ItemLength;
}
// bIsFirstItem = true, 첫번째 아이템이 화면에 얼마나 보이는지 계산하고,
// 그 계산한 값을 ItemsInView 더한다. (그 크기만큼 더하는 거임)
// 뷰에 보이는 아이템의 개수를 추적(부분적으로 보이는 아이템을 포함해서)
if (bIsFirstItem) //시작위치일 경우에는
{
// 첫 번째 아이템은 완전히 보이지 않을 수도 있다.(단, 이 값은 1.0f을 넘을 수 없음)
// FirstItemFractionScrolledIntoView 는 리스트뷰의 위쪽(또는 왼쪽)으로 스크롤되어
// 잘려나간 부분을 고려하여, 그 아이템이 화면에 보이는 비율(= 아이템의 가시 영역 비율)을
// 나타낸 값
// 예를 들어, CurrentScrollOffset = 2.3f -> 0.3f 보임..
// 그러면, 남아 있는 부분은 0.7f 잘림 => ItemLength * 0.7 => 실제 보이는 영역을 구함
const float FirstItemFractionScrolledIntoView = 1.0f - (float)FMath::Max(DoubleFractional(CurrentScrollOffset), 0.0);
// FirstItemLengthScrolledIntoView 는 리스트뷰의 위(또는 왼쪽) 밖으로
// 스크롤되어 잘려 나간 부분을 제외한, 해당 아이템의 '현재화면에 들어와 있는 길이'를 의미
// 즉, FirstItemFractionScrolledIntoView = 0.2f라면, 30px * 0.2 => 6px 가 된다.
const float FirstItemLengthScrolledIntoView = ItemLength * FirstItemFractionScrolledIntoView;
// FirstItemVisibleFraction 값은 두 경우 중 하나가 된다.
// 1. 아이템 크기가 리스트뷰 가용 공간보다 더 클 경우,
// 해당 아이템의 보이는 길이를 리스트뷰 길이로 나눈 비율 (이 경우 이 값은 1보다 클 수 있음)
// 2. 그외의 경우에는 FirstItemLengthScrolledIntoView 을 그대로 사용
const float FirstItemVisibleFraction = FMath::Min(MyDimensions.ScrollAxis / FirstItemLengthScrolledIntoView, FirstItemFractionScrolledIntoView);
ItemsInView += FirstItemVisibleFraction;
}
else if (ViewLengthUsedSoFar + ItemLength > MyDimensions.ScrollAxis)
{
// 첫번째가 아니고, 현재 리스트 뷰의 스크롤 축보다 클경우에 (보이는 축을 초과한 경우),
// 넘어간 부분은 제외하고 보이는 비율만 더한다
ItemsInView += (MyDimensions.ScrollAxis - ViewLengthUsedSoFar) / ItemLength;
}
else
{
// 중간에 위치하는 애들은 1씩 더함.
ItemsInView += 1;
}
// 지금까지 생성된 위젯들의 전체 길이
// (세로 리스트일 경우 높이, 가로 리스트일 경우에는 너비)
// 생성된 아이템의 위젯의 길이를 더함
LengthGeneratedSoFar += ItemLength;
// 뷰(화면 영역) 안에 들어와 있는 생성된 위젯들의 길이
// 즉, 화면에 실제로 보이는 (또는 일부라도 걸쳐 있는) Row들의 총 길이를 의미
// 첫번째일 경우에는 현재 아이템에 실제 보이는 아이템 길이를 곱해서 총 얼만큼 보이는지를 확인
// ItemsInView => 실제 차지하는 비율
ViewLengthUsedSoFar += (bIsFirstItem)
? ItemLength * ItemsInView // For the first item, ItemsInView <= 1.0f
: ItemLength;
// 현재 보이는 (처리할 아이템)이 마지막에 있는 아이템인지를 확인
bAtEndOfList = ItemIndex >= Items.Num() - 1;
// 첫번째 아이템이고, 이 첫 번째 아이템의 보이는 길이만으로 뷰의 사용 가능한 공간을
// 채웠거나 초과했을 경우
if (bIsFirstItem && ViewLengthUsedSoFar >= MyDimensions.ScrollAxis)
{
// 부동 소수점 합산이 없었으므로, 하나의 요소가 공간을 완전히 채우는 경우를
// 정확하게 감지하도록 함. => 이 반복문을 빠져나옴. 사용 가능영역을 다채웠음 의미
bHasFilledAvailableArea = true;
}
else
{
// 참고: 사용된 치수(길이)를 합산하는 과정에서 부동 소수점의 잘림 및 덧셈으로 인해
// 누적되는 오차를 처리하기 위해, 사용 가능한 공간을 채웠는지 확실하게 확인하기 위해
// 할당된 축에 약간의 여유분을 더함
const float FloatPrecisionOffset = 0.001f;
// 약간의 여유분을 더해서 사용 가능 영역을 다 채웠는지 여부를 확인
bHasFilledAvailableArea = ViewLengthUsedSoFar >= MyDimensions.ScrollAxis + FloatPrecisionOffset;
}
}
// 시나리오 B를 다룬다.
// 아이템의 끝에 도달했기 때문에 멈췄을 수도 있지만, 여전히 채워야 할 공간이 남아 있을 수도 있음.
if (bAtEndOfList && !bHasFilledAvailableArea)
{
// 새로운 시작 인덱스를 계산하기 위한 공식
// 새로운 뷰포트의 시작 인덱스가 어디여야 하는지.
// 예를 들어, 현재 시작인덱스가 5이고, 초과된 길이가 아이템 0.5개 길이라면,
// 이 5.5는 스크롤이 현재 5번 아이템의 절반 지점에 위치하고 있음을 의미
double NewScrollOffsetForBackfill = static_cast<double>(StartIndex) + (LengthGeneratedSoFar - MyDimensions.ScrollAxis) / FirstItemLength;
// StartIndex = 0 이여서 ItemIndex가 음수가 되는 경우를 ItemIndex >= 0로 방지
// 이전껄 채워넣는 방식임. 왜냐면 ItemIndex 가 끝에 도달했기 때문에 음수로 하나씩 줄이면서
// 새로운 아이템을 삽입
// Backfill = 되채움을 의미하며, 역순 순회로 이전걸 채워넣는 방식을 의미
for (int32 ItemIndex = StartIndex - 1; LengthGeneratedSoFar < MyDimensions.ScrollAxis && ItemIndex >= 0; --ItemIndex)
{
const ItemType& CurItem = Items[ItemIndex];
// 유효한 아이템값
if (TListTypeTraits<ItemType>::IsPtrValid(CurItem))
{
// 아이템에 대한 위젯을 생성 또는 풀링된 값을 가져옴
const float ItemLength = GenerateWidgetForItem(CurItem, ItemIndex, StartIndex, LayoutScaleMultiplier);
if (LengthGeneratedSoFar + ItemLength > MyDimensions.ScrollAxis && ItemLength > 0.f)
{
// 뷰포트 길이를 초과하게 만드는 아이템을 생성했다.
// 리스트 바깥으로 튀어나올 이 아이템의 비율을 계산한다.
// 부분적으로 보여야 하는 부분을 감지해야함.
NewScrollOffsetForBackfill = static_cast<double>(ItemIndex) + (LengthGeneratedSoFar + ItemLength - MyDimensions.ScrollAxis) / ItemLength;
}
// The widget used up some of the available vertical space.
LengthGeneratedSoFar += ItemLength;
}
}
// 생성된 결과를 전달.
return FReGenerateResults(NewScrollOffsetForBackfill, LengthGeneratedSoFar, Items.Num() - NewScrollOffsetForBackfill, true);
}
return FReGenerateResults(CurrentScrollOffset, LengthGeneratedSoFar, ItemsInView, false);
}
return FReGenerateResults(0.0f, 0.0f, 0.0f, false);
}
/** 위젯 재생성에 대한 결과의 정보 */
struct FReGenerateResults
{
FReGenerateResults(double InNewScrollOffset, double InLengthGenerated, double InItemsOnScreen, bool AtEndOfList)
: NewScrollOffset(InNewScrollOffset)
, LengthOfGeneratedItems(InLengthGenerated)
, ExactNumLinesOnScreen(InItemsOnScreen)
, bGeneratedPastLastItem(AtEndOfList)
{}
// 실제로 사용하는 스크롤 옵셋 값은 사용자가 요청한 것과 다를 수 있음.
double NewScrollOffset = 0.;
// 아이템들의 보이는 부분집합을 나타내기 위해 우리가 생성한 위젯들의, 스크롤 축을 따른 총 길이
double LengthOfGeneratedItems = 0.;
// 소수 점 이하를 포함하여(즉, 일부분) 화면에 들어가는 줄의 수
double ExactNumLinesOnScreen = 0.;
// 뷰포트를 채울 만큼 충분한 아이템을 생성할때 true
bool bGeneratedPastLastItem = false;
};
enum class EScrollIntoViewResult
{
/** 함수가 지정된 아이템을 뷰에 스크롤하는 데 성공했거나, 이미 뷰 안에 있었던 경우 */
Success,
/** 주어진 아이템을 뷰로 스크롤하기 위한 충분한 정보가 아직 없어,
다음 Tick까지 스크롤을 지연(Defer)해야 하는 경우 */
Deferred,
/** 함수가 지정된 아이템으로 스크롤하는 데 실패한 경우 */
Failure
};
// 리스트뷰 가상화 핵심 부분
// STableViewBase <- SListView
void STableViewBase::Tick( const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime )
{
if (ItemsPanel.IsValid())
{
FGeometry PanelGeometry = FindChildGeometry( AllottedGeometry, ItemsPanel.ToSharedRef() );
// 이전 패넬 범위가 현재 패넬 범위와 다르다면,
bool bPanelGeometryChanged = PanelGeometryLastTick.GetLocalSize() != PanelGeometry.GetLocalSize();
// 아이템이 변화가 있거나, 패넬 자체가 변경되었다면
if ( bItemsNeedRefresh || bPanelGeometryChanged)
{
// 업데이트된 페넬에 대해 FGeometry 저장 -> 패넬이 변경되었을 때에만 다시 그리기 위해서임.
// 가상화 부분을 의미
PanelGeometryLastTick = PanelGeometry;
// 뷰에서 스크롤 축에 직교하는 축 기준으로, 한 줄에 몇 개의 아이템을 배치하는가를 반환
// Listview -> 한 줄에 1개, Tileview -> 한 줄에 여러 개
const int32 NumItemsPerLine = GetNumItemsPerLine();
// 스크롤 결과 여부를 의미함. 현재 지오메트리에서 스크롤을 했는가(안했으면 항상 성공)
const EScrollIntoViewResult ScrollIntoViewResult = ScrollIntoView(PanelGeometry);
// 어디까지 스크롤해야 하는가를 의미하며, 스크롤한 속도값을 계산해서 어떤 행을 보여줄지 결정
// 스크롤이 최종적으로 도달해야 하는 목표 위치를 계산하는 함수
// 스크롤의 속도, 고정 라인 정렬, 스크롤 모드 등에 따라서 조정되며, 어떤 Row(Line)을 보여줄 지 결정
double TargetScrollOffset = GetTargetScrollOffset();
// 곧 멈출 예정인가
if (InertialScrollManager.GetShouldStopScrollNow())
{
// 멈출 때 목표 위치를 동일하게 지정 (미세한 차이를 방지)
TargetScrollOffset = DesiredScrollOffset = CurrentScrollOffset;
InertialScrollManager.ResetShouldStopScrollNow(); //멈췄다고 정지.
}
// 첫 터치 인터랙션 && 터치에 대한 스크롤 애니메이팅이 가능하며
// bStartedTouchInteraction : 사용자가 이 리스트 내에서 상호작용을 시작했는가
// bEnableAnimatedScrolling : 원하는 스크롤 오프셋이 변경될때 오프셋들 사이를 부드럽게 선형 보간하기 위해 true 설정
// bEnableTouchAnimatedScrolling : 터치로 인해 원하는 스크롤 오프셋이 변경될 때, 오프셋들 사이를 부드럽게 lerp 하기 위해 true로 설정
if((bStartedTouchInteraction && bEnableTouchAnimatedScrolling) || (!bStartedTouchInteraction && bEnableAnimatedScrolling))
{
// 사용자의 상호작용 방식에 따라 스크롤 움직임에 애니메이션을 적용할지 여부를 결정
CurrentScrollOffset = FMath::FInterpTo(CurrentScrollOffset, TargetScrollOffset, (double)InDeltaTime, ScrollingAnimationInterpolationSpeed);
if (FMath::IsNearlyEqual(CurrentScrollOffset, TargetScrollOffset, 0.01))
{
CurrentScrollOffset = TargetScrollOffset;
}
}
else
{
CurrentScrollOffset = TargetScrollOffset;
}
// 이 부분이 해당 페넬에 대해 아이템을 다시 생성하는 부분
// 즉, 가상화 위치로, 해당 위치에서 실제 패넬 보이는 부분에 아이템을 생성함
const FReGenerateResults ReGenerateResults = ReGenerateItems( PanelGeometry );
LastGenerateResults = ReGenerateResults;
// 실제로 다시 생성된 아이템 위젯에 대한 결과물을 이전 결과물로 캐싱
// 실제 아이템의 개수 (TArray 내의 총 아이템 개수)
const int32 NumItemsBeingObserved = GetNumItemsBeingObserved();
// NumItemsPerLine: 한 줄에 들어가는 아이템 개수
// 즉 세로 스크롤 시, X축에 아이템 몇 개 배치할건가. ListView일 때 1개 TileView일 때는 N개
const int32 NumItemLines = NumItemsBeingObserved / NumItemsPerLine;
// 실제 화면에 들어간 총 줄의 개수가 출력
const double InitialDesiredOffset = DesiredScrollOffset;
const bool bEnoughRoomForAllItems = ReGenerateResults.ExactNumLinesOnScreen >= NumItemLines;
if (bEnoughRoomForAllItems)
{
// 모든 아이템들이 보일 수 있다, 그래서 스크롤링을 없앰.
SetScrollOffset(0.0);
CurrentScrollOffset = TargetScrollOffset = DesiredScrollOffset;
}
else if (ReGenerateResults.bGeneratedPastLastItem) // Backfill 일경우에만 true
{
// 현재 스크롤 옵셋을 NewScrollOffset지정 (즉, 새롭게 계산된 스크롤 위치)
// BackFill을 고려해 시작 위치가 변경
SetScrollOffset(FMath::Max(0.0, ReGenerateResults.NewScrollOffset));
CurrentScrollOffset = TargetScrollOffset = DesiredScrollOffset;
}
// 현재 스크롤 위치 / 아이템 개수 나눠, 첫번째 줄 스크롤 위치를 분석
// 정수 값을 뺌으로서 소수점을 가져옴. 즉 첫번째 줄에서 얼만큼 부분적으로 보이는지 비율 계산
// 0.0 - 1.0 사이의 비율 계산
ItemsPanel->SetFirstLineScrollOffset(GetFirstLineScrollOffset());
// 오버 스크롤이 가능하다. 즉, 아이템 보다 더 아래로 내리거나 위로 올릴 수 있음
if (AllowOverscroll == EAllowOverscroll::Yes)
{
// 얼마나 오버 스크롤 했는지 양을 확인한다.
const float OverscrollAmount = Overscroll.GetOverscroll(GetTickSpaceGeometry());
ItemsPanel->SetOverscrollAmount( OverscrollAmount );
}
// 리스트에서 사라진 아이템 제거
// 현재 리스트에서 더이상 존재하지 않는 항목들은 선택된 항목 목록에서 제거
// 리스트 뷰 내부에서는 유효한 아이템이 아닐 경우에 삭제
// 유효한 아이템일 경우에는 SelectedItems 포함되어 있으면, 새로운 NewSelectedItem 판정
// 새로운 아이템과 원본 아이템간의 차이점이 있는지 여부를 확인 한 후 있음 업데이트
// 더이상 리스트에 보이지 않는데, 선택된 아이템 값이 유효하면 안되기 때문에 업데이트를 진행하는 것
UpdateSelectionSet();
// 스크롤바를 업데이트한다.
// 현재 실제 위젯의 위치에 대해서 스크롤 바 상태를 업데이트하는 로직이 구현
if (NumItemsBeingObserved > 0)
{
if (ReGenerateResults.ExactNumLinesOnScreen < 1.0f)
{
// 가용 가시 영역보다 더 큰 단일 행을 관찰하고 있으므로,
// 이를 기반으로 엄지(스크롤 바 Thumb) 크기를 계산해야 함.
const double VisibleSizeFraction = AllottedGeometry.GetLocalSize().Y / ReGenerateResults.LengthOfGeneratedItems;
const double ThumbSizeFraction = FMath::Min(VisibleSizeFraction, 1.0);
const double OffsetFraction = CurrentScrollOffset / NumItemsBeingObserved;
ScrollBar->SetState( OffsetFraction, ThumbSizeFraction );
}
else
{
// 엄지 크기는 우리가 현재 보고 있는 아이템들의 분수(비율)만큼이다. (부분적으로 보이는 아이템 포함)
// 예를들어, 첫번째 생성된 위젯의 0.5와 마지막 위젯의 0.75가 보인다면, 이는 1.25개의 위젯이 보이는 것
const double ThumbSizeFraction = ReGenerateResults.ExactNumLinesOnScreen / NumItemLines;
const double OffsetFraction = CurrentScrollOffset / NumItemsBeingObserved;
ScrollBar->SetState( OffsetFraction, ThumbSizeFraction );
}
}
else
{
const double ThumbSizeFraction = 1;
const double OffsetFraction = 0;
ScrollBar->SetState( OffsetFraction, ThumbSizeFraction );
}
// 바닥으로부터의 길이가 SMALL_NUMBER 작으면, 끝에 있음을 의미
bWasAtEndOfList = (ScrollBar->DistanceFromBottom() < SMALL_NUMBER);
// 아이템을 리프래쉬할 필요가 있는가?
bItemsNeedRefresh = false;
ItemsPanel->SetRefreshPending(false);
Invalidate(EInvalidateWidget::ChildOrder);
if (ScrollIntoViewResult == EScrollIntoViewResult::Success)
{
// 스크롤해야 할 작업이 남아 있더라도, 아이템에 대한 위젯을 만들자마자 즉시 알림.
NotifyItemScrolledIntoView();
}
if (ScrollIntoViewResult == EScrollIntoViewResult::Deferred || CurrentScrollOffset != TargetScrollOffset)
{
// 아이템 위젯을 아직 만들지 못했거나,
// 스크롤 작업이 남아있기 때문에 다음 프레임에 또 다른 갱신이 필요할 것.
// bItemsNeedRefresh를 단순히 true로 남겨놓는 대신에 이 함수를 호출하는 것은
// STableViewBase::EnsureTickToRefresh가 확실하게 등록되도록 보장하기 위함.
// 리스트 뷰의 갱신 로직을 현재 함수에서 즉시 실행하지 않고, 다음 프레임의 특정 시점까지 지연
// 한 프레임에 두 번 갱신이 발생하는 것을 막고, 갱신을 틱 단계로 통합하여 단 한번만 수행
// 하도록 보장함으로써 렌더링 부하를 줄이고 효율성을 높임.
RequestLayoutRefresh();
}
else if (CurrentScrollOffset == TargetScrollOffset)
{
// 현재 옵셋값이 목표로 한 옵셋값과 같으면 스크롤링을 멈춤.
NotifyFinishedScrolling();
}
// 아이템이 다시 그려졌음을 의미
OnItemsRebuilt.ExecuteIfBound();
}
}
}
리스트 뷰와 타일 뷰의 장점인 가상화가 어떻게 일어나는지를 코드로 확인해 보았다.
결론적으로,
소스코드를 하나 하나 읽어보면서 주석으로 정리했고, 실제로 틀린 부분이 있을 수도 있습니다! 만약, 틀린 부분이 있으면 꼭 알려주시길 바랍니다. (같이 정보 알아요!!!)
긍정이 언니님 멋지다 !!!