CustomScrollView를 사용하다 보면
SliverPersistentHeader를 사용하여 뷰포트 상단에 닿으면 상단에 고정(Pin) 되는 위젯을 구현해야 할 때가 생긴다.
하지만, 이러한 경우 SliverPersistentHeaderDelegate에 minExtent와 maxExtent를 수동적으로 지정해줘야 한다.
물론, layout 관련 에러가 뜨지 않게끔 맞춰서 할 수 있지만 꽤나 피곤하고 이게 맞나?라는 생각을 지울 순 없었다.
그래서 찾아봤다.
우선 Chat GPT 3에게 도움을 요청했지만 실효성 있는 결과물은 도출해 내지 못했다.
(내가 질문을 못하는 걸 지도..?)
그래서 결국 전통의 방식(?)인 구글 검색을 했다.
StackOverflow도 방문하고 Github도 방문하고 결국 찾아내었다.
https://pub.dev/packages/sliver_tools
sliver_tools | Flutter Package
A set of useful sliver tools that are missing from the flutter framework
pub.dev
위 라이브러리에서 SliverPinnedHeader를 사용하면 단순히 child만 넣으면 된다.
SliverPersistentHeaderDelegate와 다르게 minExtent, maxExtent를 지정하지 않아도 된다는 점이
아주 속 시원하다.
하지만 SliverPinnedHeader 하나 사용하자고 sliver_tools를 설치하기는 반골 성향인 나에겐 불편하게 느껴졌다
그래서! 코드를 뜯어보고 그대로 가져왔다. (사실 코드 길면 그냥 설치하려 했음)
class SliverPinnedHeader extends SingleChildRenderObjectWidget {
const SliverPinnedHeader({
Key? key,
required Widget child,
}) : super(key: key, child: child);
@override
RenderSliverPinnedHeader createRenderObject(BuildContext context) {
return RenderSliverPinnedHeader();
}
}
class RenderSliverPinnedHeader extends RenderSliverSingleBoxAdapter {
@override
void performLayout() {
child!.layout(constraints.asBoxConstraints(), parentUsesSize: true);
double childExtent;
switch (constraints.axis) {
case Axis.horizontal:
childExtent = child!.size.width;
break;
case Axis.vertical:
childExtent = child!.size.height;
break;
}
final paintedChildExtent = min(
childExtent,
constraints.remainingPaintExtent - constraints.overlap,
);
geometry = SliverGeometry(
paintExtent: paintedChildExtent,
maxPaintExtent: childExtent,
maxScrollObstructionExtent: childExtent,
paintOrigin: constraints.overlap,
scrollExtent: childExtent,
layoutExtent: max(0.0, paintedChildExtent - constraints.scrollOffset),
hasVisualOverflow: paintedChildExtent < childExtent,
);
}
@override
double childMainAxisPosition(RenderBox child) {
return 0;
}
}
위 코드를 내가 사용할 프로젝트에 옮겨 넣은 뒤 사용하면 된다.
추가적으로 나는 SliverPinnedHeader의 child가 상태(상단에 붙어있는지)에 따라 바뀌길 원하기 때문에 아래와 같이 조금 수정했다.
class SliverSwitchingPinnedHeader extends HookWidget{
const SliverSwitchingPinnedHeader({Key? key,required this.child, this.pinnedChild}) : super(key: key);
final Widget child;
final Widget? pinnedChild;
@override
Widget build(BuildContext context){
final state = useState(false);
return SliverPinnedHeader(
onChangePinnedState: (pinned){
if(state.value != pinned){
state.value = pinned;
}
},
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: state.value ? pinnedChild ?? child : child,
),
);
}
}
class SliverPinnedHeader extends SingleChildRenderObjectWidget {
final ValueChanged<bool>? onChangePinnedState;
const SliverPinnedHeader({
Key? key,
required child,
this.onChangePinnedState,
}) : super(key: key, child: child);
@override
RenderObject createRenderObject(BuildContext context) {
return RenderSliverPinnedHeader(
onChangePinnedState: onChangePinnedState,
);
}
}
class RenderSliverPinnedHeader extends RenderSliverSingleBoxAdapter {
bool _pinnedState = false;
final ValueChanged<bool>? onChangePinnedState;
RenderSliverPinnedHeader({this.onChangePinnedState});
@override
void performLayout() {
child!.layout(constraints.asBoxConstraints(), parentUsesSize: true);
double childExtent;
switch (constraints.axis) {
case Axis.horizontal:
childExtent = child!.size.width;
break;
case Axis.vertical:
childExtent = child!.size.height;
break;
}
final paintedChildExtent = min(
childExtent,
constraints.remainingPaintExtent - constraints.overlap,
);
geometry = SliverGeometry(
paintExtent: paintedChildExtent,
maxPaintExtent: childExtent,
maxScrollObstructionExtent: childExtent,
paintOrigin: constraints.overlap,
scrollExtent: childExtent,
layoutExtent: max(0.0, paintedChildExtent - constraints.scrollOffset),
hasVisualOverflow: paintedChildExtent < childExtent,
);
SchedulerBinding.instance.addPostFrameCallback((timeStamp) {
bool newPinnedState =
constraints.scrollOffset <= 0;
newPinnedState = !newPinnedState;
if (_pinnedState != newPinnedState) {
_pinnedState = newPinnedState;
onChangePinnedState?.call(_pinnedState);
}
});
}
@override
double childMainAxisPosition(RenderBox child) {
return 0;
}
}
위 코드와 같이하면 상태(Boolean) 값에 따라 두 가지 위젯을 나타낼 수 있다.
다만 필자는 Riverpod를 사용하기 때문에 그렇지 않으신 분들이라면
HookWidget을 StatefulWidget으로 변환하여 사용하면 될 것이다.
'Dart > Flutter' 카테고리의 다른 글
[crop_your_image] 이미지 크롭 라이브러리 커스텀하기 (0) | 2023.08.11 |
---|---|
[Flutter][KakaoLogin] 플러터 카카오 로그인 구현 (1. 설정) (0) | 2023.04.23 |
[Flutter] Deep Link (0) | 2022.11.23 |
[Flutter] apply? copyWith? in TextTheme (0) | 2022.11.18 |
[Flutter] Widget에 Border 추가하기 (0) | 2022.11.17 |
댓글