flutter輪子計(jì)劃之進(jìn)度條
前言
本文的記錄如何用CustomPaint、GestureDetector實(shí)現(xiàn)一個(gè)進(jìn)度條控件。首先需要說(shuō)明的是 flutter Material 組件庫(kù)中提供了兩種進(jìn)度指示器:LinearProgressIndicator和CircularProgressIndicator。如果這兩種進(jìn)度指示器可以滿足開(kāi)發(fā)需求,就不要嘗試自己造輪子了。本文實(shí)現(xiàn)的進(jìn)度條控件,功能如下:
- 進(jìn)度的范圍為0到1的double類型數(shù)據(jù)
- 支持拖動(dòng),通過(guò)回調(diào)函數(shù)獲取進(jìn)度值
- 支持跳轉(zhuǎn),點(diǎn)擊某個(gè)位置后進(jìn)度跳轉(zhuǎn),回調(diào)進(jìn)度值
- 樣式為Material風(fēng)格的樣式,可以根據(jù)需要修改
識(shí)別拖動(dòng)手勢(shì)
使用GestureDetector可以方便得對(duì)滑動(dòng),點(diǎn)擊事件進(jìn)行監(jiān)聽(tīng)。如下是監(jiān)聽(tīng)的四個(gè)事件,重點(diǎn)關(guān)注onHorizontalDragUpdate即可,其回調(diào)函數(shù)將水平拖動(dòng)事件的坐標(biāo)等信息傳遞給_seekToRelativePosition函數(shù)。_seekToRelativePosition函數(shù)的功能是計(jì)算滑動(dòng)時(shí)進(jìn)度條的值,并更新界面。代碼如下:
GestureDetector( onHorizontalDragStart: (DragStartDetails details) { widget.onDragStart?.call(); }, onHorizontalDragUpdate: (DragUpdateDetails details) { widget.onDragUpdate?.call(); _seekToRelativePosition(details.globalPosition); }, onHorizontalDragEnd: (DragEndDetails details) { widget.onDragEnd?.call(progress); }, onTapDown: (TapDownDetails details) { widget.onTapDown?.call(progress); _seekToRelativePosition(details.globalPosition); }, // .... )
_seekToRelativePosition 將全局坐標(biāo)轉(zhuǎn)換為進(jìn)度條控件所在的舉動(dòng)坐標(biāo)。將點(diǎn)擊處的橫坐標(biāo),即x與進(jìn)度條控件的長(zhǎng)度的比率作為進(jìn)度條的值。然后調(diào)用setState()更新界面。上面
void _seekToRelativePosition(Offset globalPosition) { final box = context.findRenderObject()! as RenderBox; final Offset tapPos = box.globalToLocal(globalPosition); progress = tapPos.dx / box.size.width; if (progress < 0) progress = 0; if (progress > 1) progress = 1; setState(() { widget.controller.progressBarValue = progress; }); }
上面代碼中有一個(gè)controller控件,其定義如下:
class VideoProgressBarController extends ChangeNotifier { double progressBarValue = .0; updateProgressValue(double value){ progressBarValue = value; notifyListeners(); } }
其繼承自ChangeNotifier, 因?yàn)榇诉M(jìn)度條控件的狀態(tài)由其他控件和控件本身混合管理狀態(tài)。當(dāng)其他控件想改變進(jìn)度條的值時(shí),可以通過(guò)VidoeProgressBarController通知進(jìn)度條控件更新界面。當(dāng)然,將進(jìn)度條控件改用statelesswidget實(shí)現(xiàn),然后直接調(diào)用setState()更新界面實(shí)現(xiàn)起來(lái)會(huì)更簡(jiǎn)單一點(diǎn),讀者有需要可以嘗試。
使用CustomPaint繪制進(jìn)度條
繪制部分比較簡(jiǎn)單。如下,先繪制灰色背景,然后繪制紅色的進(jìn)度,再回事圓點(diǎn)。
class _VideoProgressBarPainter extends CustomPainter { _VideoProgressBarPainter( {required this.barHeight, required this.handleHeight, required this.value, required this.colors}); final double barHeight; final double handleHeight; final ProgressColors colors; final double value; @override bool shouldRepaint(CustomPainter painter) { return true; } @override void paint(Canvas canvas, Size size) { final baseOffset = size.height / 2 - barHeight / 2; final double radius = 4.0; canvas.drawRRect( RRect.fromRectAndRadius( Rect.fromPoints( Offset(0.0, baseOffset), Offset(size.width, baseOffset + barHeight), ), const Radius.circular(4.0), ), colors.backgroundPaint, ); double playedPart = value > 1 ? size.width - radius : value * size.width - radius; if (playedPart < radius) { playedPart = radius; } canvas.drawRRect( RRect.fromRectAndRadius( Rect.fromPoints( Offset(0.0, baseOffset), Offset(playedPart, baseOffset + barHeight), ), Radius.circular(radius), ), colors.playedPaint, ); canvas.drawCircle( Offset(playedPart, baseOffset + barHeight / 2), handleHeight, colors.playedPaint, ); } }
完整代碼:
import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; void main() { runApp(MyApp()); } class MyApp extends StatelessWidget { // This widget is the root of your application. @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', theme: ThemeData( primarySwatch: Colors.blue, ), home: MyHomePage(title: 'Flutter Demo Home Page'), ); } } class MyHomePage extends StatefulWidget { MyHomePage({Key? key, required this.title}) : super(key: key); final String title; @override _MyHomePageState createState() => _MyHomePageState(); } class _MyHomePageState extends State<MyHomePage> { double _progressValue = .5; late VideoProgressBarController controller; @override void initState() { controller = VideoProgressBarController(); super.initState(); } @override Widget build(BuildContext context) { print("build:$_progressValue"); return SafeArea( child: Scaffold( appBar: AppBar(title: Text("test")), body: Column( //aspectRatio: 16 / 9, children: [ Container( width: 200, height: 26, //color: Colors.blue, child: VideoProgressBar( controller: controller, barHeight: 2, onDragEnd: (double progress) { print("$progress"); }, ), ), Text("value:$_progressValue"), ElevatedButton( onPressed: (){ _progressValue = 1; controller.updateProgressValue(_progressValue); }, child: Text("increase") ) ] ), ), ); } } /// progress bar class VideoProgressBar extends StatefulWidget { VideoProgressBar({ ProgressColors? colors, Key? key, required this.controller, required this.barHeight, this.handleHeight = 6, this.onDragStart, this.onDragEnd, this.onDragUpdate, this.onTapDown, }) : colors = colors ?? ProgressColors(), super(key: key); final ProgressColors colors; final Function()? onDragStart; final Function(double progress)? onDragEnd; final Function()? onDragUpdate; final Function(double progress)? onTapDown; final double barHeight; final double handleHeight; final TVideoProgressBarController controller; //final bool drawShadow; @override _VideoProgressBarState createState() => _VideoProgressBarState(); } class _VideoProgressBarState extends State<VideoProgressBar> { double progress = .0; @override void initState() { super.initState(); progress = widget.controller.progressBarValue; widget.controller.addListener(_updateProgressValue); } @override void dispose() { widget.controller.removeListener(_updateProgressValue); super.dispose(); } _updateProgressValue() { setState(() { progress = widget.controller.progressBarValue; }); } void _seekToRelativePosition(Offset globalPosition) { final box = context.findRenderObject()! as RenderBox; final Offset tapPos = box.globalToLocal(globalPosition); progress = tapPos.dx / box.size.width; if (progress < 0) progress = 0; if (progress > 1) progress = 1; setState(() { widget.controller.progressBarValue = progress; }); } @override Widget build(BuildContext context) { final size = MediaQuery.of(context).size; return GestureDetector( onHorizontalDragStart: (DragStartDetails details) { widget.onDragStart?.call(); }, onHorizontalDragUpdate: (DragUpdateDetails details) { widget.onDragUpdate?.call(); _seekToRelativePosition(details.globalPosition); }, onHorizontalDragEnd: (DragEndDetails details) { widget.onDragEnd?.call(progress); }, onTapDown: (TapDownDetails details) { widget.onTapDown?.call(progress); _seekToRelativePosition(details.globalPosition); }, child: Center( child: Container( height: MediaQuery.of(context).size.height, width: MediaQuery.of(context).size.width, child: CustomPaint( painter: _VideoProgressBarPainter( barHeight: widget.barHeight, handleHeight: widget.handleHeight, colors: widget.colors, value: progress)), ), )); } } class _VideoProgressBarPainter extends CustomPainter { _VideoProgressBarPainter( {required this.barHeight, required this.handleHeight, required this.value, required this.colors}); final double barHeight; final double handleHeight; final ProgressColors colors; final double value; @override bool shouldRepaint(CustomPainter painter) { return true; } @override void paint(Canvas canvas, Size size) { final baseOffset = size.height / 2 - barHeight / 2; final double radius = 4.0; canvas.drawRRect( RRect.fromRectAndRadius( Rect.fromPoints( Offset(0.0, baseOffset), Offset(size.width, baseOffset + barHeight), ), const Radius.circular(4.0), ), colors.backgroundPaint, ); double playedPart = value > 1 ? size.width - radius : value * size.width - radius; if (playedPart < radius) { playedPart = radius; } canvas.drawRRect( RRect.fromRectAndRadius( Rect.fromPoints( Offset(0.0, baseOffset), Offset(playedPart, baseOffset + barHeight), ), Radius.circular(radius), ), colors.playedPaint, ); canvas.drawCircle( Offset(playedPart, baseOffset + barHeight / 2), handleHeight, colors.playedPaint, ); } } class VideoProgressBarController extends ChangeNotifier { double progressBarValue = .0; updateProgressValue(double value){ progressBarValue = value; notifyListeners(); } } class ProgressColors { ProgressColors({ Color playedColor = const Color.fromRGBO(255, 0, 0, 0.7), Color bufferedColor = const Color.fromRGBO(30, 30, 200, 0.2), Color handleColor = const Color.fromRGBO(200, 200, 200, 1.0), Color backgroundColor = const Color.fromRGBO(200, 200, 200, 0.5), }) : playedPaint = Paint()..color = playedColor, bufferedPaint = Paint()..color = bufferedColor, handlePaint = Paint()..color = handleColor, backgroundPaint = Paint()..color = backgroundColor; final Paint playedPaint; final Paint bufferedPaint; final Paint handlePaint; final Paint backgroundPaint; }
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
Android實(shí)現(xiàn)滑動(dòng)效果
這篇文章主要為大家詳細(xì)介紹了Android實(shí)現(xiàn)滑動(dòng)效果,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-09-09Android中AlarmManager+Notification實(shí)現(xiàn)定時(shí)通知提醒功能
本篇文章主要介紹了Android中AlarmManager+Notification實(shí)現(xiàn)定時(shí)通知提醒功能,非常具有實(shí)用價(jià)值,需要的朋友可以參考下2017-10-10Android沉浸式狀態(tài)欄實(shí)現(xiàn)
這篇文章主要介紹了Android沉浸式狀態(tài)欄實(shí)現(xiàn),即一體化狀態(tài)欄實(shí)現(xiàn),感興趣的小伙伴們可以參考一下2016-01-01android Imageview 圖片覆蓋具體實(shí)現(xiàn)
android Imageview 圖片覆蓋實(shí)現(xiàn)及注意事項(xiàng)如下,感興趣的朋友可以參考下哈2013-06-06android項(xiàng)目實(shí)現(xiàn)帶進(jìn)度條的系統(tǒng)通知欄消息
本篇文章主要介紹了android項(xiàng)目實(shí)現(xiàn)帶進(jìn)度條的系統(tǒng)通知欄消息,就是實(shí)現(xiàn)在通知欄看到下載進(jìn)度。具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下。2016-10-10Android 通過(guò)代碼設(shè)置、打開(kāi)wifi熱點(diǎn)及熱點(diǎn)連接的實(shí)現(xiàn)代碼
這篇文章主要介紹了Android 通過(guò)代碼設(shè)置、打開(kāi)wifi熱點(diǎn)及熱點(diǎn)連接的實(shí)現(xiàn)代碼,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),需要的朋友可以參考下2018-05-05Android App打包加固后的APK無(wú)法安裝問(wèn)題解決
Android應(yīng)用當(dāng)中,很多隱私信息都是以 字符串的形式存在的,所以需要加密,本文主要介紹了Android App打包加固后的APK無(wú)法安裝問(wèn)題解決,感興趣的可以了解一下2024-01-01RecyclerView實(shí)現(xiàn)探探卡片滑動(dòng)效果
這篇文章主要為大家詳細(xì)介紹了RecyclerView實(shí)現(xiàn)探探卡片滑動(dòng)效果,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-01-01