實現這個效果很簡單,只需要加一行css代碼即可:
-webkit-overflow-scrolling : touch;
可用以下網頁測試:
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<meta charset="utf-8" />
<title>scroll</title>
<style type="text/css">
.container {
width : 300px;
height : 50%;
-webkit-box-sizing : border-box;
position : relative;
overflow-y : auto;
background-color : cyan;
-webkit-overflow-scrolling : touch; /* liuhx:可以把這整行注釋掉對比差別 */
}
ul {
height: 50px;
}
</style>
</head>
<body>
<div align="center">
<nav class="container">
<ul>1</ul>
<ul>2</ul>
<ul>3</ul>
<ul>4</ul>
<ul>5</ul>
<ul>6</ul>
<ul>7</ul>
<ul>8</ul>
<ul>9</ul>
<ul>10</ul>
<ul>11</ul>
<ul>12</ul>
<ul>13</ul>
<ul>14</ul>
<ul>15</ul>
<ul>16</ul>
<ul>17</ul>
<ul>18</ul>
<ul>19</ul>
<ul>20</ul>
</nav>
</div>
</body>
</html>
評論
不久看到這樣一個很有趣的效果,它的滾動條是沿著圓角邊緣滾動的,效果如下
你可以查看原鏈接來體驗一下
https://codepen.io/jh3y/pen/gOEgxbd
這是如何實現的呢?
原效果中由于為了兼容不支持CSS滾動驅動的瀏覽器,特意用 JS做了兼容,所以看著比較復雜,其實核心非常簡單,下面我將用最簡短的 CSS 來復刻這一效果,一起看看吧
從本質上來講,其實是一個 SVG 路徑動畫。
具體如何實現呢?
首先,我們通過設計軟件繪制一個這樣的路徑
注意設置描邊的大小還有端點的類型,比如下面是round效果
然后導出SVG,可以得到這樣一段代碼
<svg viewBox="0 0 31 433" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4 4C9.96737 4 15.6903 6.37053 19.9099 10.5901C24.1295 14.8097 26.5 20.5326 26.5 26.5V406.5C26.5 412.467 24.1295 418.19 19.9099 422.41C15.6903 426.629 9.96737 429 4 429" stroke="black" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
然后,如何讓這段SVG動起來呢?
很簡單,現在SVG是一段實線,我們可以通過stroke-dasharray設置成虛線,比如
path{
stroke-dasharray: 80
}
這樣會得到一個實線和虛線間隔都是80的虛線
如果希望虛線空白的地方更大一點,該怎么設置呢?很簡單,繼續往后加
path{
stroke-dasharray: 80 120
}
效果如下
所以,這種寫法其實相當于把當前的值無限重復,示意如下
當然,我們這里不需要設置的這么復雜,只需要一小段實線就夠了,所以是實現加上一段足夠長的虛線(超過路徑本身就行),實現如下
path{
stroke-dasharray: 80 1000
}
這樣就得到了一小段實線
那么,如何讓他動起來呢?很簡單,改變一下偏移就可以,這個可以用stroke-dashoffset來實現
比如,我們
@keyframes scroll {
to {
stroke-dashoffset: -370
}
}
path{
stroke-dasharray: 80 1000;
animation: scroll 3s alternate-reverse infinite;
}
效果如下
是不是有點像呢?
我們再調整一下起始偏移量,讓它出去一點
@keyframes scroll {
0% { stroke-dashoffset: 75; }
100% { stroke-dashoffset: -445; }
}
這樣就更接近我們想要的效果了
整個運動原理就是這樣了,接著往下看
接下來需要通過滾動驅動動畫將容器滾動與CSS動畫「聯動」起來。
關于CSS 滾動驅動可以參考我之前寫的這篇文章:CSS 滾動驅動動畫終于正式支持了~
簡單來講,「CSS 滾動驅動動畫」指的是將「動畫的執行過程由頁面滾動」進行接管,也就是這種情況下,「動畫只會跟隨頁面滾動的變化而變化」,也就是滾動多少,動畫就執行多少,「時間不再起作用」。
先簡單布局一下
<div class="list">
<div class="item" id="item_1">1</div>
<div class="item" id="item_2">2</div>
<div class="item" id="item_3">3</div>
<div class="item" id="item_4">4</div>
<div class="item" id="item_5">5</div>
<div class="item" id="item_6">6</div>
<div class="item" id="item_7">7</div>
</div>
美化一下
然后,我們將默認的滾動條隱藏,用我們這個 SVG路徑來代替,由于需要絕對定位,我們再套一層父級
<div class="wrap">
<div class="list">
<div class="item" id="item_1">1</div>
<div class="item" id="item_2">2</div>
<div class="item" id="item_3">3</div>
<div class="item" id="item_4">4</div>
<div class="item" id="item_5">5</div>
<div class="item" id="item_6">6</div>
<div class="item" id="item_7">7</div>
<!--滾動條-->
<svg class="scroller" viewBox="0 0 31 433" fill="none" xmlns="http://www.w3.org/2000/svg">
<path class="scroller_thumb" d="M4 4C9.96737 4 15.6903 6.37053 19.9099 10.5901C24.1295 14.8097 26.5 20.5326 26.5 26.5V406.5C26.5 412.467 24.1295 418.19 19.9099 422.41C15.6903 426.629 9.96737 429 4 429" stroke="black" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</div>
</div>
相關CSS如下
.wrap{
position: relative;
}
.scroller {
position: absolute;
top: 0;
bottom: 0;
right: 0;
pointer-events: none;
height: -webkit-fill-available;
margin: 5px;
}
.scroller_thumb{
stroke: hsl(0 0% 100% / 0.5);
stroke-dasharray: 80 450;
stroke-width: 8px;
animation: scroll both 5s linear;
}
這樣結構就搭好了,只是滾動條會自動播放
接下來就是最關鍵的一步,加上滾動驅動動畫
.scroller_thumb{
animation: scroll both 5s linear;
animation-timeline: scroll();
}
但是這樣是不起作用的,直接使用scroll()會自動尋找它的相對父級,也就是.wrap,但實際滾動的其實是.list,所以這種情況下我們需要具名的滾動時間線,實現如下
.list{
scroll-timeline: --scroller;
}
.scroller_thumb{
animation: scroll both 5s linear;
animation-timeline: --scroller;
}
這樣SVG路徑動畫就能跟隨容器滾動而運動了
原效果中還有一個滾動回彈的效果,當滾動到容器邊緣時,會自動回彈到起始位置。
其實只需要用到 CSS scroll snap 就可以了
https://developer.mozilla.org/zh-CN/docs/Web/CSS/scroll-snap-type
實現很簡單,給滾動容器添加scroll-snap-type屬性,表示這是個允許滾動吸附的容器
.list{
scroll-snap-type: y mandatory;
}
然后就指定需要吸附的點了,由于需要回彈的效果,所以滾動容器的首尾需要一個空白的容器,這里直接用兩個偽元素來生成
.list::before,
.list::after{
content: '';
height: 50px;
flex-shrink: 0;
}
效果如下
然后我們設置滾動吸附點就行了,設置第一個元素頂部和最后一個元素底部,其他元素居中就行了
.item{
scroll-snap-align: center;
}
.item:first-child{
scroll-snap-align: start;
}
/*最后一個元素是 SVG,所以這里用倒數第二個元素*/
.item:nth-last-child(2){
scroll-snap-align: end;
}
這樣就實現了文章開頭的效果了
完整代碼可以查看以下鏈接(無任何 JS)
總的來說,CSS滾動驅動在滾動交互上帶來了無限可能,很多以前必須借助 JS來實現的都可以輕易實現,下面總結一下
作者:XboxYan
來源:微信公眾號:前端偵探
出處:https://mp.weixin.qq.com/s/GaakgWhXm6jpY4PfISNHZQ
篇會深化View拖拽實例,利用Flutter Animation、插值器以及AnimatedBuilder教大家實現帶動畫的抽屜效果。先來看效果:
通過構思,我們可以設想到實現抽屜的方式就是用Stack控件將兩個Widget疊加顯示,用GestureDetector監聽手勢滑動,動態移動頂層的Widget,當監聽到手勢結束的時候根據手勢滑動的距離動態將頂部Widget利用動畫效果滑動到結束位置即可。
實現底部Widget
class DownDrawerWidget extends StatelessWidget { @override Widget build(BuildContext context) { return Container(child: Center(child: Text("底部Widget",),),); } }
這個Widget太簡單了,就不細說了。
實現頂部Widget
class UpDrawerWidget extends StatelessWidget { @override Widget build(BuildContext context) { return Container(child: Center(child: Text("頂部Widget",),),); } }
實現方式和底部是一樣的。
實現可以移動的容器
上面兩個Widget都是單純用來顯示的Widget,因此繼承了StatelessWidget。接下來我們需要根據手勢動態移動頂部的Widget,因此需要繼承StatefulWidget。
// 頂部Widget class HomePageWidget extends StatefulWidget { @override State<StatefulWidget> createState()=> HomePageState(); } class HomePageState extends State<HomePageWidget> with SingleTickerProviderStateMixin { @override void initState() {...} @override void dispose() {...} @override Widget build(BuildContext context) {...} void _onViewDragDown(DragDownDetails callback) {...} void _onViewDrag(DragUpdateDetails callback) {...} void _onViewDragUp(DragEndDetails callback) {...} }
初始化狀態initState()
這個方法是在Widget初始化的時候系統的回調函數,我們需要在該函數中初始化動畫
AnimationController controller; @override void initState() { // 初始化動畫控制器,這里限定動畫時常為200毫秒 controller=new AnimationController(vsync: this, duration: const Duration(milliseconds: 200)); // vsync對象會綁定動畫的定時器到一個可視的widget,所以當widget不顯示時,動畫定時器將會暫停,當widget再次顯示時,動畫定時器重新恢復執行,這樣就可以避免動畫相關UI不在當前屏幕時消耗資源。 // 當使用vsync: this的時候,State對象必須with SingleTickerProviderStateMixin或TickerProviderStateMixin;TickerProviderStateMixin適用于多AnimationController的情況。 // 設置動畫曲線,就是動畫插值器 // 通過這個鏈接可以了解更多差值器,https://docs.flutter.io/flutter/animation/Curves-class.html,我們這里使用帶回彈效果的bounceOut。 CurvedAnimation curve=new CurvedAnimation(parent: controller, curve: Curves.bounceOut); // 增加動畫監聽,當手勢結束的時候通過動態計算到達目標位置的距離實現動畫效果。curve.value為當前動畫的值,取值范圍0~1。 curve.addListener(() { double animValue=curve.value; double offset=dragUpDownX - dragDownX; double toPosition; // 右滑 if (offset > 0) { if (offset > maxDragX / 5) { // 打開 toPosition=maxDragX; isOpenState=true; } else { if (isOpenState) { toPosition=maxDragX; isOpenState=true; } else { toPosition=0.0; isOpenState=false; } } } else { if (offset < (-maxDragX / 2.0)) { // 關 toPosition=0.0; isOpenState=false; } else { if (isOpenState) { toPosition=maxDragX; isOpenState=true; } else { toPosition=0.0; isOpenState=false; } } } dragOffset=(toPosition - dragUpDownX) * animValue + dragUpDownX; // 刷新位置 setState(() {}); }); }
結束Widget dispose()
當Widget不可用將被回收的時候,系統會回調dispose()方法,我們在這里回收動畫。
@override void dispose() { controller.dispose(); }
記錄按下的位置
double dragDownX=0.0; void _onViewDragDown(DragDownDetails callback) { dragDownX=callback.globalPosition.dx; }
拖動的時候刷新View的位置
/** * 最大可拖動位置 */ final double maxDragX=230.0; double dragOffset=0.0; void _onViewDrag(DragUpdateDetails callback) { double tmpOffset=callback.globalPosition.dx - dragDownX; if (tmpOffset < 0) { tmpOffset +=maxDragX; } // 邊緣檢測 if (tmpOffset < 0) { tmpOffset=0.0; } else if (tmpOffset >=maxDragX) { tmpOffset=maxDragX; } // 刷新 if (dragOffset !=tmpOffset) { dragOffset=tmpOffset; setState(() {}); } }
離手的時候記錄位置并執行動畫
/** * 脫手時候的位置 */ double dragUpDownX=0.0; void _onViewDragUp(DragEndDetails callback) { dragUpDownX=dragOffset; // 執行動畫,每次都從第0幀開始執行 controller.forward(from: 0.0); }
支持移動的Widget
@override Widget build(BuildContext context) { return Transform.translate( offset: Offset(dragOffset, 0.0), child: Container( child: GestureDetector( onHorizontalDragDown: _onViewDragDown, onVerticalDragDown: _onViewDragDown, onHorizontalDragUpdate: _onViewDrag, onVerticalDragUpdate: _onViewDrag, onHorizontalDragEnd: _onViewDragUp, onVerticalDragEnd: _onViewDragUp, child: Container( child: new UpDrawerWidget(), ),),),);}
Flutter動畫
總結一下,想在Flutter中實現動畫,需要先創建一個AnimationController控制器;如果有特殊的插值要求,再創建一個插值器,調用controller.forward()方法執行動畫,通過addListener()的回調改變對應數值之后調用setState(() {})方法刷新位置即可。
Flutter API還提供AnimatedBuilder用來簡化實現動畫的復雜性,讓我們不用手動調用addListener()方法。
*請認真填寫需求信息,我們會在24小時內與您取得聯系。