自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

一文讓你理清PrimaryScrollController

移動開發(fā)
對蘋果用戶來說,大家基本都知道,iOS手機應用有一個比較常見的功能:點擊狀態(tài)欄,列表就會滾動到頂部。在iOS原生代碼中,我們可以通過原生框架的已有特性或者自己添加監(jiān)聽來實現(xiàn)這個功能。

PrimaryScrollController的作用

對蘋果用戶來說,大家基本都知道,iOS手機應用有一個比較常見的功能:點擊狀態(tài)欄,列表就會滾動到頂部。

在iOS原生代碼中,我們可以通過原生框架的已有特性或者自己添加監(jiān)聽來實現(xiàn)這個功能。

那么在flutter中有沒有呢?答案當然是肯定的。

flutter專門為iOS端做了這一個支持,可以讓我們快速的實現(xiàn)點擊狀態(tài)欄回頂部的效果,它就是一系列圍繞PrimaryScrollController數(shù)據(jù)傳遞方式所展開的設計。

按照我們早期flutter開發(fā)經(jīng)驗,如果沒有仔細的對PrimaryScrollController和相關類的實現(xiàn)有詳細的了解,必然會在構(gòu)建結(jié)構(gòu)復雜的頁面時出現(xiàn)各種奇怪的問題。

PrimaryScrollController的定義

PrimaryScrollController的源碼內(nèi)容并不多,主要包含兩部分。

  • 擴展自InheritedWidget
  • 持有ScrollController類型的變量

下面是源碼部分:

class PrimaryScrollController extends InheritedWidget {

const PrimaryScrollController({
Key? key,
required ScrollController this.controller,
required Widget child,
}) : assert(controller != null),
super(key: key, child: child);


const PrimaryScrollController.none({
Key? key,
required Widget child,
}) : controller = null,
super(key: key, child: child);

final ScrollController? controller;


static ScrollController? of(BuildContext context) {
final PrimaryScrollController? result = context.dependOnInheritedWidgetOfExactType<PrimaryScrollController>();
return result?.controller;
}
...
}

關于InheritedWidget

InheritedWidget可以說是flutter框架內(nèi)比較常見的數(shù)據(jù)傳遞設計抽象,簡單介紹一下。

?

每個Element實例都持有一個_inheritedWidgets?,每當要為Widget添加特定類型的依賴時,就會從該集合里取出相關類型的InheritedElement實例。

而element的_inheritedWidgets是在每次element掛載和重新啟用時,element都會從它的上層element中打包拿到其所持有的所有_inheritedWidgets。

還有特殊的InheritedElement? 它繼承了Element?,相較于普通的Element,InheritedElement?不僅會拿到其上層element所有的_inheritedWidgets,而且會將自己也作為一個元素添加到集合中

自定義 InheritedWidgetA:

class InheritedWidgetA extends InheritedWidget {
Value a;
...
static Value? of(BuildContext context) {
final InheritedWidgetA? result =
context.dependOnInheritedWidgetOfExactType<InheritedWidgetA>();
return result?.a;
}
}

使用示例和數(shù)據(jù)傳遞如下:

圖片

inheritedWidget數(shù)據(jù)圖

如上圖所示:childA,childB都能共享上級樹的數(shù)據(jù)。

ScrollController

ScrollController?間接繼承自Listenable,主要有兩個功能

  • 監(jiān)聽滾動事件
  • 控制列表滾動

ScrollController部分實現(xiàn):

class ScrollController extends ChangeNotifier {
...
void jumpTo(double value) {
assert(_positions.isNotEmpty, 'ScrollController not attached to any scroll views.');
for (final ScrollPosition position in List<ScrollPosition>.of(_positions))
position.jumpTo(value);
}




void attach(ScrollPosition position) {
assert(!_positions.contains(position));
_positions.add(position);
position.addListener(notifyListeners);
}




void detach(ScrollPosition position) {
assert(_positions.contains(position));
position.removeListener(notifyListeners);
_positions.remove(position);
}

}

看源碼發(fā)現(xiàn):

ScrollController?提供了綁定和解綁ScrollPosition?。 每個ScrollPosition?對應一個Scrollable?滾動視圖 ,注意ScrollController?是可以綁定多個ScrollPosition。

所以通過scrollController.position直接取值報錯可能是大多數(shù)朋友會踩的坑。

ScrollPosition get position {
assert(_positions.isNotEmpty, 'ScrollController not attached to any scroll views.');
assert(_positions.length == 1, 'ScrollController attached to multiple scroll views.');
return _positions.single;
}

ScrollView與ScrollController的聯(lián)系:

ScrollView?創(chuàng)建時是需要兩個參數(shù)controller和primary?的,主要用來確定綁定的scrollController是使用controller?還是最近的父級PrimaryScrollController中的scrollController。

abstract class ScrollView extends StatelessWidget {
final ScrollController? controller;
final bool primary;

@override
Widget build(BuildContext context) {
final List<Widget> slivers = buildSlivers(context);
final AxisDirection axisDirection = getDirection(context);


final ScrollController? scrollController =
primary ? PrimaryScrollController.of(context) : controller;

final Scrollable scrollable = Scrollable(
controller: scrollController,
);
...
return scrollable;
}

}

可以看到在ScrollView?中會創(chuàng)建Scrollable,Scrollable?會在_updatePosition?時與ScrollController?進行綁定,接著ScrollController就能控制視圖滾動,或者監(jiān)聽視圖滾動。

class ScrollableState extends State<Scrollable> with TickerProviderStateMixin, RestorationMixin
implements ScrollContext {




ScrollPosition get position => _position!;
ScrollPosition? _position;

final _RestorableScrollOffset _persistedScrollOffset = _RestorableScrollOffset();

@override
AxisDirection get axisDirection => widget.axisDirection;

late ScrollBehavior _configuration;
ScrollPhysics? _physics;
ScrollController? _fallbackScrollController;
MediaQueryData? _mediaQueryData;

ScrollController get _effectiveScrollController => widget.controller ?? _fallbackScrollController!;


void _updatePosition() {
_configuration = widget.scrollBehavior ?? ScrollConfiguration.of(context);
_physics = _configuration.getScrollPhysics(context);
if (widget.physics != null) {
_physics = widget.physics!.applyTo(_physics);
} else if (widget.scrollBehavior != null) {
_physics = widget.scrollBehavior!.getScrollPhysics(context).applyTo(_physics);
}
final ScrollPosition? oldPosition = _position;
if (oldPosition != null) {
_effectiveScrollController.detach(oldPosition);



scheduleMicrotask(oldPosition.dispose);
}

_position = _effectiveScrollController.createScrollPosition(_physics!, this, oldPosition);
assert(_position != null);
_effectiveScrollController.attach(position);
}

}

到這里已經(jīng)介紹完了PrimaryScrollController?的實現(xiàn)以及相關的類與其的關系,接下來,我們看一下Flutter官方是怎么利用PrimaryScrollController?來設計點擊狀態(tài)欄回頂部功能的,看看Flutter還在哪些內(nèi)部組件埋下了關于PrimaryScrollController的處理。

Scaffold

到目前為止,我們只談了PrimaryScrollController的使用,那么思考一下:點擊狀態(tài)欄事件的監(jiān)聽是在哪里實現(xiàn)的?是如何對應到每個具體頁面的?

你猜對了,在Scaffold中。Scaffold是基于Material上的一種視覺支架,可以很方便的作出類似iOS風格的交互和UI。Flutter官方在Scaffold中添加了狀態(tài)欄區(qū)域的gesture并處理了點擊事件??丛创a:

class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin, RestorationMixin {

@override
Widget build(BuildContext context) {
...
switch (themeData.platform) {
case TargetPlatform.iOS:
case TargetPlatform.macOS:
_addIfNonNull(
children,
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: _handleStatusBarTap,

excludeFromSemantics: true,
),
_ScaffoldSlot.statusBar,
removeLeftPadding: false,
removeTopPadding: true,
removeRightPadding: false,
removeBottomPadding: true,
);
break;
case TargetPlatform.android:
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
case TargetPlatform.windows:
break;
}
...
}




void _handleStatusBarTap() {
final ScrollController? _primaryScrollController = PrimaryScrollController.of(context);
if (_primaryScrollController != null && _primaryScrollController.hasClients) {
_primaryScrollController.animateTo(
0.0,
duration: const Duration(milliseconds: 300),
curve: Curves.linear,
);
}
}

}

可以看到,Scaffold中添加了狀態(tài)欄位置的點擊,并在點擊后通過 PrimaryScrollController.of(context) 獲取scrollController,最后調(diào)整滾動位置。

此時我們已經(jīng)知道了狀態(tài)欄監(jiān)聽使用PrimaryScrollController.of(context)?進行了控制滾動,ScrollView 綁定了PrimaryScrollController.of(context) 。

好了,到目前為止,我們可以看下面的例子:一般情況下我們的項目代碼是下面這樣

runApp(
MaterialApp(
theme: ThemeData(
platform: TargetPlatform.iOS,
primarySwatch: Colors.blue,
),
routes: kkConfigureRoutes(),
initialRoute: "/",
)
);


class PageAState {
Widget build(BuildContext context) {
super.build(context);
return Scaffold(
child:ListView(
primary:true
controller:null
...
)
);
}
}

當你push到PageA時,接著點擊狀態(tài)欄,PageA中的列表回到了頂部。感覺好像沒什么問題,但是好像缺了點什么,對嗎?

對!?? 你發(fā)現(xiàn)了,我們并沒有創(chuàng)建PrimaryScrollController? 和 Scrollcontroller。那么Scaffold中取的PrimaryScrollController來自哪里?

PrimaryScrollController  的默認創(chuàng)建

在上面PageAState中你會發(fā)現(xiàn):PrimaryScrollController.of(context) 是有值的。所以答案只能是在push到頁面pageA時,就創(chuàng)建了PrimaryScrollController和Scrollcontroller。猜測flutter應該是在router層給大家自動創(chuàng)建了。我們尋找一下源碼,發(fā)現(xiàn)在routes.dart的_ModalScope中,套了一層 PrimaryScrollController(controller:primaryScrollController)。

class _ModalScopeState<T> extends State<_ModalScope<T>> {




....
final ScrollController primaryScrollController = ScrollController();

@override
Widget build(BuildContext context) {
return ...
child: PrimaryScrollController(
controller: primaryScrollController,
...
)
}
}

路由每產(chǎn)生一級ModalScopeState,會創(chuàng)建ScrollController(), 并添加PrimaryScrollController Widget。頁面Page作為子Wideget就可以獲取到上級的ScrollController。

使用流程小結(jié)

上面講了這么多,現(xiàn)在我們可以總結(jié)一下,正確優(yōu)雅的使用官方提供的點擊狀態(tài)欄功能的步驟:

  • 需要通過路由進了頁面
  • 頁面需要使用Scaffold, 這里注意(同一個頁面Scaffold不能嵌套,否則可能無法響應狀態(tài)欄點擊事件)
  • Scaffold中有ScrollView
  • PrimaryScrollController.of(context) 綁定了ScrollView

這樣就實現(xiàn)了點擊狀態(tài)欄滾動視圖回到頂部功能。

實際問題

我們來看一個比較常見的App結(jié)構(gòu):打開app,app底部有三個tab,每個tab都有對應的A,B兩個列表頁。下面是代碼:

void main {
runApp(
MaterialApp(
routes: kkConfigureRoutes(),
initialRoute: "/",
)
);
}



class RootTabPageState extends BaseThemeState<RootTabPage> {

late PageController _pageController;
late List<Widget> _tabs;

@override
void initState() {
super.initState();
_tabs = [
PageA(key: _),
PageB(key: _),
];
}

@override
Widget build(BuildContext context) {

return Scaffold(
child:Column(
children:[
Expanded(child: PageView(
children: _tabs,
controller: _pageController,
physics: const NeverScrollableScrollPhysics()
)),
KKBottomBar(...),
]
),
);
}

}



class PageAState with AutomaticKeepAliveClientMixin {
Widget build(BuildContext context) {
super.build(context);
return ListView(
primary:true
controller:null
...
);
}
...
}

class PageBState with AutomaticKeepAliveClientMixin {
Widget build(BuildContext context) {
super.build(context);
return ListView(
primary:true
controller:null
...
);
}
...
}

上面的代碼有點特殊問題,不知道你們發(fā)現(xiàn)沒有:如果我點擊狀態(tài)欄,頁面的列表會滾動到頂部嗎?分析一下,有Router層創(chuàng)建了PrimaryScrollController,RootTabPageState層包裝了Scaffold監(jiān)聽點擊狀態(tài)欄事件,然后A,B頁面primary=true , 兩個頁面的ScrollView都綁定了父PrimaryScrollController.of(context)。所以點擊狀態(tài)欄,列表會回到頂部。但是你會發(fā)現(xiàn)PrimaryScrollController.of(context) 綁定了兩個ScrollView。所以點擊狀態(tài)欄,兩個列表都會回到頂部,當然如果需求是這樣,那么沒問題,但是我想大部分情況下這是一個問題。所以,我們來試著改一下:

class RootTabPageState extends BaseThemeState<RootTabPage> {

late PageController _pageController;
late List<Widget> _tabs;

@override
void initState() {
super.initState();
_tabs = [
PageA(key: _),
PageB(key: _),
PageC(key: _),
];
}

@override
Widget build(BuildContext context) {

return Material(
child:Column(
children:[
Expanded(child: PageView(
children: _tabs,
controller: _pageController,
physics: const NeverScrollableScrollPhysics()
)),
KKBottomBar(...),
]
),
);
}

}

class PageAState {
Widget build(BuildContext context) {
return Scaffold(
ListView(
primary:true
controller:null
...
)
);
}
...
}

class PageBState {
Widget build(BuildContext context) {

return Scaffold(
ListView(
primary:true
controller:null
...
)
);
}
...
}

我們將RootTabPageState中的Scaffold改成了Material,A,B頁面加上了Scaffold。想想結(jié)果是什么?雖然我們添加了兩個Scaffold監(jiān)聽各自的頁面A,B。但是PrimaryScrollController.of(context) ,其實是Router層創(chuàng)建的,所以PrimaryScrollController.of(context) 還是綁定了兩個頁面的ScrollView。所以點擊狀態(tài)欄,兩個列表都會回到頂部。我們繼續(xù)調(diào)整:

class RootTabPageState extends BaseThemeState<RootTabPage> {

late PageController _pageController;
late List<Widget> _tabs;

@override
void initState() {
super.initState();
_tabs = [
PageA(key: _),
PageB(key: _),
PageC(key: _),
];
}

@override
Widget build(BuildContext context) {

return Material(
child:Column(
children:[
Expanded(child: PageView(
children: _tabs,
controller: _pageController,
physics: const NeverScrollableScrollPhysics()
)),
KKBottomBar(...),
]
),
);
}

}

class PageAState {

final ScrollController _scrollController = ScrollController();

Widget build(BuildContext context) {
return
PrimaryScrollController(
controller: _scrollController,
child: Scaffold(
ListView(
primary:true
controller:null
...
)
)
);
}

...
}

class PageBState {
final ScrollController _scrollController = ScrollController();

Widget build(BuildContext context) {
return
PrimaryScrollController(
controller: _scrollController,
child: Scaffold(
ListView(
primary:true
controller:null
...
)
)
);
}
...
}

我們在A,B頁面自己添加了PrimaryScrollController并創(chuàng)建了_scrollController,這樣PageA中的Scaffold取PrimaryScrollController.of(context) 其實取的是我們創(chuàng)建的_scrollController。PageA中的Scrollview綁定的也是PageA中的_scrollController。所以現(xiàn)在,我們在A頁面點擊狀態(tài)欄,那么只有A頁面的列表會回到頂部了。當大家真正了解了上面提到的相關內(nèi)容后,在你遇到不同的頁面結(jié)構(gòu)時,就知道如何去設計,才能避免一些奇怪的問題。

大家可以思考一下?在上述例子結(jié)構(gòu)中,如果其中PageAState頁面不止包含一個列表,而是本身是一個可以左右滾動的多列表時,該如何實現(xiàn)在頁面A點擊狀態(tài)欄,讓頁面A當前顯示的列表回到頂部。

篇幅有限,這里給提供一個思路,每個列表單獨創(chuàng)建ScrollController。PageA層自定義ScrollController類,重寫其滾動方法來接受狀態(tài)欄點擊事件,下發(fā)到對應列表的ScrollController。

隱秘的問題

接下來,說一個比較隱秘的問題,下面是一個例子:

class PageAState {
final ScrollController _scrollController = ScrollController();

Widget build(BuildContext context) {
super.build(context);
return
PrimaryScrollController(
controller: _scrollController,
Scaffold(
child:ListView(
primary:true
controller:null,
children:[
CellA()
])

);
}
}


class CellAState {

ScrollController? _controller;
@override
Widget build(BuildContext context) {
_controller = PrimaryScrollController.of(context);
return MyButton(
onPress:_press
);
}

void _press(){
_controller?.jumpTo(0);
}
}

上面這個例子中,我想在CellAState中獲取_controller,然后用它來做點事情,比如里面有個按鈕,然后點擊后,讓列表滾動到某個位置。雖然這個例子看起來非常簡單,但是很不幸,你取到的_controller為null,為什么?此時你會檢查代碼,檢查PrimaryScrollController的使用方式是否有問題,在檢查了一輪之后,發(fā)現(xiàn)并沒有問題,然后你可能開始有點抓狂。這個例子層級少,比較簡單的,我們可以也許可以通過斷點發(fā)現(xiàn)一些端倪,但是在項目中可能層級非常之多,如果通過斷點去找,那將是地獄。沒有辦法,你只能進入地獄,很幸運我們的例子很簡單,這個地獄不是特別深,通過斷點一步一步的,你會發(fā)現(xiàn)有一個 PrimaryScrollController.none,回顧一下,這個東西好像在PrimaryScrollController的源碼中出現(xiàn)過。

這個東西是在哪創(chuàng)建的呢???

因為我們例子比較簡單,所以我們能肯定問題發(fā)生在List中,但是在項目中這將是一個非常隱秘的問題。我們進入listView, 一層層的進入,最后看到了它的抽象類ScrollView,我們之前提到過。我們再來看下ScrollView的源碼:

abstract class ScrollView extends StatelessWidget {
final ScrollController? controller;
final bool primary;
const ScrollView({
Key? key,
this.controller,
bool? primary,
...
}) : assert(scrollDirection != null),
assert(!(controller != null && (primary ?? false)),
'Primary ScrollViews obtain their ScrollController via inheritance from a PrimaryScrollController widget. '
'You cannot both set primary to true and pass an explicit controller.',
),
primary = primary ?? controller == null && identical(scrollDirection, Axis.vertical),
super(key: key);

@override
Widget build(BuildContext context) {
...
final ScrollController? scrollController =
primary ? PrimaryScrollController.of(context) : controller;
final Scrollable scrollable = Scrollable(
...
);
final Widget scrollableResult = primary && scrollController != null
? PrimaryScrollController.none(child: scrollable)
: scrollable;
...
return scrollableResult;
}

}

我們發(fā)現(xiàn)了什么?

final Widget scrollableResult = primary && scrollController != null
? PrimaryScrollController.none(child: scrollable)
: scrollable;

在 primary && scrollController != null的情況下它為我們包裝了一層PrimaryScrollController.none(child: scrollable)  等效于 PrimaryScrollController(controller:null,child:scrollable)。也就是按照我們外部傳prmary = true的情況下,它把我們截斷了。所以回到我們的問題,如果我們要在CellA中想通過PrimaryScrollController.of(context)取值,該如何修改?

class PageAState {
final ScrollController _scrollController = ScrollController();


Widget build(BuildContext context) {
super.build(context);
return
PrimaryScrollController(
controller: _scrollController,
Scaffold(
child:ListView(
primary:false
controller:_scrollController,
children:[
CellA()
])

);
}
}


class CellAState {

ScrollController? _controller;
@override
Widget build(BuildContext context) {
_controller = PrimaryScrollController.of(context);
return MyButton(
onPress:_press
);
}

void _press(){
_controller?.jumpTo(0);
}
}

結(jié)語

好了,本篇基本已經(jīng)到了尾聲了,相信大家以后碰到與PrimaryScrollController相關的問題便不再是問題了。

看完了這一系列內(nèi)容,我們可以發(fā)現(xiàn)PrimaryScrollController?只是flutter設計的一種數(shù)據(jù)傳遞的方案,只是解決點擊狀態(tài)欄使列表滾動到頂部這個問題中的一環(huán)。整個問題其實是涉及到了ScroView,ScrollController,Scaffold?以及Router中的_ModalScopeState等,它們或多或少的提供了特殊處理和輔助方式。

不得不說flutter的組件提供了非常強大的功能,但這也可能導致看似無關的組件和類之間,內(nèi)部其實是有一定聯(lián)系的,而且比較隱蔽,所以在部分復雜場景下,可能會出現(xiàn)一些問題,這時候就比較考驗開發(fā)者耐心和對各種組件源碼的熟悉度了。

責任編輯:未麗燕 來源: 搜狐技術(shù)產(chǎn)品
相關推薦

2023-03-10 22:08:20

2018-05-31 20:49:50

Spark堆內(nèi)內(nèi)存優(yōu)化機制

2024-09-26 07:27:27

2018-05-21 10:20:22

人工智能機器學習神經(jīng)網(wǎng)絡

2018-02-02 11:17:42

IaaSPaaSSaaS

2019-07-03 15:32:26

路由器網(wǎng)絡系統(tǒng)

2020-11-12 09:14:25

JAVA.IO、字符編

2021-10-20 08:49:30

Vuexvue.js狀態(tài)管理模式

2018-06-13 08:33:32

車聯(lián)網(wǎng)智能交通互聯(lián)網(wǎng)

2018-10-08 15:00:27

物聯(lián)網(wǎng)LoRaIOT

2021-09-10 16:10:21

panda透視表語言

2019-11-06 17:00:51

深度學習神經(jīng)網(wǎng)絡人工智能

2022-03-18 09:45:43

Git分支Linux

2018-02-03 09:59:20

python程序編輯器

2021-01-21 14:26:56

大數(shù)據(jù)互聯(lián)網(wǎng)大數(shù)據(jù)應用

2025-02-28 06:35:47

2025-01-17 10:49:44

數(shù)字化轉(zhuǎn)型數(shù)字化企業(yè)管理

2024-08-12 12:30:27

2023-08-03 09:55:58

筆記本庫存新驅(qū)動

2023-11-08 07:56:38

單鏈表雙鏈表
點贊
收藏

51CTO技術(shù)棧公眾號