此文章主要介紹怎么使用Flutter的Cupertino風格控件往扔,寫一個iOS風格的通訊錄,還有在此過程中遇到的問題及解決辦法窑滞。
大家在用Flutter寫App的時候定血,一般都會使用material風格的控件转砖,因為material風格的控件比較豐富须鼎,但是,他在iOS上就會顯得Android氣息比較重府蔗,不太適合晋控,所以本文章將通過用仿寫iOS通訊錄,系統(tǒng)地介紹Cupertino控件礁竞,及系統(tǒng)的一些底層控件和怎么自己定義優(yōu)美的適合自己的控件糖荒。
由于使用的聯(lián)系人三方包的限制,有些功能未能實現(xiàn)模捂,我會持續(xù)關注這個聯(lián)系人插件的更新捶朵,及時加上新功能。
首頁
主要用到的控件及問題
CupertinoPageScaffold
一個iOS風格Scaffold狂男,可以添加NavigationBar综看。
NestedScrollView
實現(xiàn)浮動的NavigationBar和SearchBar。
NestedScrollView我用的自己重寫過的岖食,主要是因為源碼中的有兩個問題红碑。
1、當列表滑動到底部泡垃,然后繼續(xù)滑動析珊,然后停止,松手蔑穴,這時候可列表會重新滾動到底部忠寻,但是源碼沒有處理當速度等于0的時候的情況,所以當松手的時候存和,列表會回彈回去奕剃,回彈距離小于maxScrollExtent
。
源碼如下:
@protected
ScrollActivity createInnerBallisticScrollActivity(_NestedScrollPosition position, double velocity) {
return position.createBallisticScrollActivity(
position.physics.createBallisticSimulation(
velocity == 0 ? position as ScrollMetrics : _getMetrics(position, velocity),
velocity,
),
mode: _NestedBallisticScrollActivityMode.inner,
);
}
這里當velocity == 0
的時候捐腿,直接把innerPosition賦值給了createBallisticSimulation
方法的position
參數(shù)纵朋,我們繼續(xù)往下看。
ScrollActivity createBallisticScrollActivity(
Simulation simulation, {
@required _NestedBallisticScrollActivityMode mode,
_NestedScrollMetrics metrics,
}) {
if (simulation == null) return IdleScrollActivity(this);
assert(mode != null);
switch (mode) {
case _NestedBallisticScrollActivityMode.outer:
assert(metrics != null);
if (metrics.minRange == metrics.maxRange) return IdleScrollActivity(this);
return _NestedOuterBallisticScrollActivity(
coordinator,
this,
metrics,
simulation,
context.vsync,
);
case _NestedBallisticScrollActivityMode.inner:
return _NestedInnerBallisticScrollActivity(
coordinator,
this,
simulation,
context.vsync,
);
case _NestedBallisticScrollActivityMode.independent:
return BallisticScrollActivity(this, simulation, context.vsync);
}
return null;
}
這里velocity == 0
的時候茄袖,執(zhí)行的是
case _NestedBallisticScrollActivityMode.inner:
return _NestedInnerBallisticScrollActivity(
coordinator,
this,
simulation,
context.vsync,
);
這時候的simulation
就是上面通過innerPosition得到的操软,然后傳給了_NestedInnerBallisticScrollActivity
,我們在繼續(xù)往下看宪祥,
class _NestedInnerBallisticScrollActivity extends BallisticScrollActivity {
_NestedInnerBallisticScrollActivity(
this.coordinator,
_NestedScrollPosition position,
Simulation simulation,
TickerProvider vsync,
) : super(position, simulation, vsync);
final _NestedScrollCoordinator coordinator;
@override
_NestedScrollPosition get delegate => super.delegate as _NestedScrollPosition;
@override
void resetActivity() {
delegate.beginActivity(coordinator.createInnerBallisticScrollActivity(
delegate,
velocity,
));
}
@override
void applyNewDimensions() {
delegate.beginActivity(coordinator.createInnerBallisticScrollActivity(
delegate,
velocity,
));
}
@override
bool applyMoveTo(double value) {
return super.applyMoveTo(coordinator.nestOffset(value, delegate));
}
}
我們發(fā)現(xiàn)這里執(zhí)行的操作并不是我們想要的寺鸥,當velocity == 0
猪钮,滑動距離大于maxScrollExtent
的時候品山,我們只想滾動到列表的最底部胆建,所以我們改一下這里的實現(xiàn)。此處有兩種實現(xiàn)方式:
第一種方式:改_getMetrics
方法
// This handles going forward (fling up) and inner list is
// underscrolled, OR, going backward (fling down) and inner list is
// scrolled past zero. We want to skip the pixels we don't need to grow
// or shrink over.
if (velocity > 0.0) {
// shrinking
extra = _outerPosition.minScrollExtent - _outerPosition.pixels;
} else if (velocity < 0.0) {
// growing
extra = _outerPosition.pixels - (_outerPosition.maxScrollExtent - _outerPosition.minScrollExtent);
} else {
extra = 0.0;
}
assert(extra <= 0.0);
minRange = _outerPosition.minScrollExtent;
maxRange = _outerPosition.maxScrollExtent + extra;
assert(minRange <= maxRange);
correctionOffset = 0.0;
這里加上velocity == 0
的判斷肘交。
第二種方式:修改createInnerBallisticScrollActivity
方法笆载,加上velocity == 0
的判斷。
@protected
ScrollActivity createInnerBallisticScrollActivity(_NestedScrollPosition position, double velocity) {
return position.createBallisticScrollActivity(
position.physics.createBallisticSimulation(
velocity == 0 ? position as ScrollMetrics : _getMetrics(position, velocity),
velocity,
),
mode: velocity == 0 ? _NestedBallisticScrollActivityMode.independent : _NestedBallisticScrollActivityMode.inner,
);
}
2涯呻、當我們手動調用position.moveTo
方法滾動到最底部的時候凉驻,獲取到的maxScrollExtent
并不是實際innerPosition
的maxScrollExtent
,而應該是maxScrollExtent - outerPosition.maxScrollExtent + outerPosition.pixels
复罐。
接下來我們分析源碼看看哪里出了問題涝登。
首先,我們看看與之有直接關聯(lián)的maxScrollExtent方法效诅。
@override
double get maxScrollExtent => _maxScrollExtent;
我們看到只是單純的返_maxScrollExtent
胀滚,那我們看看_maxScrollExtent
是在哪里賦值的,經(jīng)過查看源碼得知乱投,_maxScrollExtent
賦值的地方主要在下面這個方法里:
@override
bool applyContentDimensions(double minScrollExtent, double maxScrollExtent) {
assert(minScrollExtent != null);
assert(maxScrollExtent != null);
if (!nearEqual(_minScrollExtent, minScrollExtent, Tolerance.defaultTolerance.distance) ||
!nearEqual(_maxScrollExtent, maxScrollExtent, Tolerance.defaultTolerance.distance) ||
_didChangeViewportDimensionOrReceiveCorrection) {
assert(minScrollExtent != null);
assert(maxScrollExtent != null);
assert(minScrollExtent <= maxScrollExtent);
_minScrollExtent = minScrollExtent;
_maxScrollExtent = maxScrollExtent;
_haveDimensions = true;
applyNewDimensions();
_didChangeViewportDimensionOrReceiveCorrection = false;
}
return true;
}
所以我們重寫這個方法咽笼,修改如下:
@override
bool applyContentDimensions(double minScrollExtent, double maxScrollExtent) {
assert(minScrollExtent != null);
assert(maxScrollExtent != null);
var outerPosition = coordinator._outerPosition;
var outerMaxScrollExtent = outerPosition.maxScrollExtent;
var outerPixels = outerPosition.pixels;
if (outerMaxScrollExtent != null && outerPixels != null) {
maxScrollExtent -= outerMaxScrollExtent - outerPixels;
maxScrollExtent = math.max(minScrollExtent, maxScrollExtent);
}
return super.applyContentDimensions(minScrollExtent, maxScrollExtent);
}
這樣我們成功解決了上面提到的兩個問題。
CustomScrollView
實現(xiàn)浮動的Index戚炫。
SliverPersistentHeader
實現(xiàn)Index固定在頭部剑刑。
CupertinoSliverRefreshIndicator
實現(xiàn)下拉刷新。
群組
新建聯(lián)系人頁面
編輯頭像
聯(lián)系人詳情
選擇標簽
至此施掏,基本完成。