flutter輪子計劃之進度條
前言
本文的記錄如何用CustomPaint、GestureDetector實現(xiàn)一個進度條控件。首先需要說明的是 flutter Material 組件庫中提供了兩種進度指示器:LinearProgressIndicator和CircularProgressIndicator。如果這兩種進度指示器可以滿足開發(fā)需求,就不要嘗試自己造輪子了。本文實現(xiàn)的進度條控件,功能如下:
- 進度的范圍為0到1的double類型數(shù)據(jù)
- 支持拖動,通過回調(diào)函數(shù)獲取進度值
- 支持跳轉(zhuǎn),點擊某個位置后進度跳轉(zhuǎn),回調(diào)進度值
- 樣式為Material風格的樣式,可以根據(jù)需要修改
識別拖動手勢
使用GestureDetector可以方便得對滑動,點擊事件進行監(jiān)聽。如下是監(jiān)聽的四個事件,重點關(guān)注onHorizontalDragUpdate即可,其回調(diào)函數(shù)將水平拖動事件的坐標等信息傳遞給_seekToRelativePosition函數(shù)。_seekToRelativePosition函數(shù)的功能是計算滑動時進度條的值,并更新界面。代碼如下:
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 將全局坐標轉(zhuǎn)換為進度條控件所在的舉動坐標。將點擊處的橫坐標,即x與進度條控件的長度的比率作為進度條的值。然后調(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; }); }
上面代碼中有一個controller控件,其定義如下:
class VideoProgressBarController extends ChangeNotifier { double progressBarValue = .0; updateProgressValue(double value){ progressBarValue = value; notifyListeners(); } }
其繼承自ChangeNotifier, 因為此進度條控件的狀態(tài)由其他控件和控件本身混合管理狀態(tài)。當其他控件想改變進度條的值時,可以通過VidoeProgressBarController通知進度條控件更新界面。當然,將進度條控件改用statelesswidget實現(xiàn),然后直接調(diào)用setState()更新界面實現(xiàn)起來會更簡單一點,讀者有需要可以嘗試。
使用CustomPaint繪制進度條
繪制部分比較簡單。如下,先繪制灰色背景,然后繪制紅色的進度,再回事圓點。
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; }
以上就是本文的全部內(nèi)容,希望對大家的學習有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
Android中AlarmManager+Notification實現(xiàn)定時通知提醒功能
本篇文章主要介紹了Android中AlarmManager+Notification實現(xiàn)定時通知提醒功能,非常具有實用價值,需要的朋友可以參考下2017-10-10android Imageview 圖片覆蓋具體實現(xiàn)
android Imageview 圖片覆蓋實現(xiàn)及注意事項如下,感興趣的朋友可以參考下哈2013-06-06android項目實現(xiàn)帶進度條的系統(tǒng)通知欄消息
本篇文章主要介紹了android項目實現(xiàn)帶進度條的系統(tǒng)通知欄消息,就是實現(xiàn)在通知欄看到下載進度。具有一定的參考價值,感興趣的小伙伴們可以參考一下。2016-10-10Android 通過代碼設(shè)置、打開wifi熱點及熱點連接的實現(xiàn)代碼
這篇文章主要介紹了Android 通過代碼設(shè)置、打開wifi熱點及熱點連接的實現(xiàn)代碼,本文通過實例代碼給大家介紹的非常詳細,需要的朋友可以參考下2018-05-05