Chuyển đến nội dung chính

Creating a custom progress indicator

 


Phụ lục:


  1. Core Widgets involved

  2. The basics

  3. Tìm hiểu sâu về Custom Paint

  • Làm thế nào để tạo một Custom Painter?

  • Breaking down the paint method

  • StartAngle và scanAngle là gì?

  • Kết quả cuối cùng

  1. Conclusion ( Phần kết luận )

Tuyên bố từ chối trách nhiệm: Tôi nghĩ rằng tôi thêm điều này chỉ trong trường hợp một số người nhầm lẫn. Hình ảnh là hình ảnh được cắt và widget thực tế chỉ là phần hình tròn chứ không phải hình chữ nhật với các góc tròn.

Xin chào! Hôm nay, tôi sẽ trình bày một điều gì đó mà tôi đã gặp phải gần đây trong một dự án mà tôi nghĩ rằng tôi sẽ chia sẻ những gì tôi đã học được và hy vọng các bạn cũng có thể học hỏi từ nó! Tôi sẽ hướng dẫn cách tạo chỉ báo tiến trình tùy chỉnh. Đây là một ví dụ về những gì tôi đang đề cập đến:


Đối với blog này, tôi cho rằng bạn có kiến thức cơ bản về các tiện ích Flutter và sẽ không đi

sâu vào chi tiết mọi thứ. Custom Paint sẽ là trọng tâm chính ở đây. Ngoài ra, trước khi tôi đi

sâu vào vấn đề này, vui lòng kiểm tra dự án tôi đang thực hiện tại đây:

https://github.com/Dan-Y-Ko/Flutter-Dart-Playground/tree/master/flutter/ui/banking_app_ui


1. Core Widgets involved ( Các tiện ích cốt lõi liên quan )


Các tiện ích con được sử dụng như sau:


  • Container

  • Stack

  • CustomPaint

2. The basics

Đầu tiên, chúng ta muốn tạo một vòng tròn thực tế. Để làm điều này, chúng ta có thể sử dụng simple container. Tuy nhiên, chúng tôi cũng muốn trùng lặp với chỉ báo tiến trình tùy chỉnh của mình, vì vậy chúng tôi cũng cần phải gói nó trong một ngăn xếp. Mã sẽ giống như sau (nó phải đơn giản):

import 'package:flutter/material.dart';

const Color darkBlue = Color.fromARGB(255, 18, 32, 47);

void main() {
runApp(MyApp());
}

class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData.dark().copyWith(scaffoldBackgroundColor: darkBlue),
debugShowCheckedModeBanner: false,
home: const Scaffold(
body: Center(
child: ProgressIndicatorButton(),
),
),
);
}
}

class ProgressIndicatorButton extends StatelessWidget {
const ProgressIndicatorButton({
Key? key,
}) : super(key: key);

@override
Widget build(BuildContext context) {
const buttonSize = 80.0;
const borderWidth = 2.0;

return Stack(
children: [
Container(
width: buttonSize,
height: buttonSize,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(
color: Colors.white,
width: borderWidth,
),
),
),
],
);
}
}

Kết quả đầu ra sẽ như thế này:





3. Tìm hiểu sâu về Custom Paint


Đây là nơi mọi thứ trở nên thú vị! Tôi hy vọng bạn nhớ lượng giác của bạn và vòng tròn đơn vị của bạn (đùa thôi, có Google). Tôi chỉ muốn thêm một tuyên bố từ chối trách nhiệm rằng bạn có thể tạo bất cứ thứ gì theo nghĩa đen với Custom Paint và xem qua mọi tình huống có thể xảy ra sẽ trở thành một blog khá dài vì vậy tôi chỉ đề cập đến một khía cạnh của nó.


Điều chính mà một tiện ích Custom Paint nên thực hiện là triển khai Custom Painter.


  • Làm thế nào để tạo một Custom Painter?


Việc triển khai Custom Painter sẽ bao gồm những điều sau đây làm cơ sở:


-mở rộng lớp Custom Painter

-thực hiện phương pháp sơn

-thực hiện phương pháp shouldRepaint


Trước khi đi qua phương pháp sơn, chúng ta hãy xem xét shouldRepaint là gì. Về cơ bản, nó giống như âm thanh của nó. Nó trả về một giá trị boolean và nếu chúng ta muốn tạo một phiên bản mới của Custom Painter này, chúng ta nên trả về true. Tuy nhiên, chúng tôi không cần điều này, vì vậy chúng tôi sẽ đặt nó thành false trong ví dụ này.


  • Breaking down the paint method


Phương pháp sơn có kích thước và sử dụng Canvas để vẽ thực tế. Để tìm hiểu thêm về những thứ khác nhau mà bạn có thể tạo, hãy xem tại đây: https://api.flutter.dev/flutter/dart-ui/Canvas-class.html.

Có rất nhiều thứ bạn có thể tạo: hình chữ nhật, hình tròn, đường thẳng và thậm chí cả đường dẫn tùy chỉnh. Đối với ví dụ này, chúng tôi sẽ tập trung vào Arc.


void paint(Canvas canvas, Size size) {
// 2
final paint = Paint()
// 3
..color = Colors.blue
// 4
..strokeCap = StrokeCap.butt
// 5
..style = PaintingStyle.stroke
// 6
..strokeWidth = width;
// 7
final center = Offset(size.width / 2, size.height / 2);
// 8
final radius = (size.width / 2) - (width / 2);
// 1
canvas.drawArc(
Rect.fromCircle(center: center, radius: radius),
startAngle,
sweepAngle,
false,
paint,
);
}

1.Như đã đề cập, chúng tôi đang tạo một Arc nên phương thức drawArc được sử dụng. Đối số thứ nhất là một lớp Rect, từ đó có một số tùy chọn nhưng chúng ta muốn một Circle và Rect.fromCircle sẽ cung cấp cho chúng ta hiệu ứng đó. Các đối số khác sẽ được thảo luận một chút. Đối số thứ hai chỉ định nơi chúng ta muốn vòng cung bắt đầu. Đối số thứ ba chỉ định nơi chúng ta muốn vòng cung kết thúc. Đối số thứ tư xác định xem chúng ta có muốn sử dụng tâm để kết nối cung tròn hay không. Điều này sẽ tạo ra một đường từ biên giới đến trung tâm, mà chúng tôi không muốn. Đối số thứ năm là đối số được tạo từ lớp Paint sẽ được thảo luận một chút. Có một cái nhìn ở đây để tham khảo: https://api.flutter.dev/flutter/dart-ui/Canvas/drawArc.html


2.Lớp Paint chịu trách nhiệm về hiệu ứng hình ảnh của chỉ báo tiến trình tùy chỉnh của chúng tôi. Điều này cần được chuyển vào phương thức drawArc.


3.Điều này tạo ra màu sắc.


4.Có một số tùy chọn ở đây, và những gì chúng tôi chọn ở đây sẽ quyết định "phần cuối" của vòng cung sẽ như thế nào. Ví dụ, StrokeCap.round sẽ bao bọc cung của chúng ta bằng một vòng tròn. Tôi thực sự không biết phải giải thích thế nào khác nên nếu bạn vẫn còn bối rối, hãy xem tại đây: https://api.flutter.dev/flutter/dart-ui/StrokeCap-class.html


5.Chúng ta có thể sử dụng tô màu hoặc nét vẽ ở đây. Chúng tôi chỉ muốn sơn màu trên một chiều rộng cụ thể, vì vậy chúng tôi sẽ sử dụng nét viền trong ví dụ này.


6.Điều này đi đôi với #5 và xác định độ dày của nét vẽ. Chúng tôi muốn chỉ định chiều rộng đi cùng với PaintingStyle.stroke.


7.Offset xác định các điểm cụ thể trên trục x và y tương ứng. Giá trị trung tâm này được chuyển vào Rect.fromCircle.


8.Chúng tôi nhận được bán kính bằng cách trừ chiều rộng của toàn bộ hình tròn và chiều rộng của "đường viền". Giá trị bán kính này được chuyển vào Rect.fromCircle.


Trước khi đi vào startAngle và sweepAngle, đây là mã của bạn sẽ trông như thế nào cho đến nay:


import 'package:flutter/material.dart';
// import 'dart:math' as math;

const Color darkBlue = Color.fromARGB(255, 18, 32, 47);

void main() {
runApp(MyApp());
}

class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData.dark().copyWith(scaffoldBackgroundColor: darkBlue),
debugShowCheckedModeBanner: false,
home: const Scaffold(
body: Center(
child: ProgressIndicatorButton(),
),
),
);
}
}

class ProgressIndicatorButton extends StatelessWidget {
const ProgressIndicatorButton({
Key? key,
// required this.startAngle,
// required this.endAngle,
}) : super(key: key);

// final double startAngle;
// final double endAngle;

@override
Widget build(BuildContext context) {
const buttonSize = 80.0;
const borderWidth = 2.0;

return Stack(
children: [
Container(
width: buttonSize,
height: buttonSize,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(
color: Colors.white,
width: borderWidth,
),
),
),
SizedBox(
width: buttonSize,
height: buttonSize,
child: CustomPaint(
painter: ProgressIndicatorPainter(
width: borderWidth,
// startAngle: startAngle,
// sweepAngle: endAngle,
),
child: Center(
child: Container(
width: buttonSize - 20.0,
height: buttonSize - 20.0,
decoration: const BoxDecoration(
color: Colors.blue,
shape: BoxShape.circle,
),
child: const Center(
child: Icon(
Icons.done,
size: 30.0,
),
),
),
),
),
),
],
);
}
}

class ProgressIndicatorPainter extends CustomPainter {
const ProgressIndicatorPainter({
required this.width,
// required this.startAngle,
// required this.sweepAngle,
}) : super();

final double width;
// final double startAngle;
// final double sweepAngle;

@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = Colors.blue
..strokeCap = StrokeCap.butt
..style = PaintingStyle.stroke
..strokeWidth = width;
final center = Offset(size.width / 2, size.height / 2);
final radius = (size.width / 2) - (width / 2);
// canvas.drawArc(
// Rect.fromCircle(center: center, radius: radius),
// startAngle,
// sweepAngle,
// false,
// paint,
// );
}

@override
bool shouldRepaint(CustomPainter oldDelegate) => false;
}

Hãy chắc chắn rằng bạn đã nhận xét đúng mã nơi tôi có, nếu không bạn sẽ gặp lỗi.


Kết quả đầu ra sẽ như thế này:




  • StartAngle và scanAngle là gì?


Ở mức tổng quan cao cấp, startAngle và scanAngle là những gì sẽ xác định nơi mà vòng cung của chúng ta sẽ bắt đầu và kết thúc (như đã đề cập trước đây).


-startAngle: Theo mặc định, vị trí bắt đầu là 0 radian trong vòng tròn đơn vị. Một điều cần lưu ý nữa là hướng trong cung này là theo chiều kim đồng hồ, thay vì ngược chiều kim đồng hồ. Thay vì những thứ quá phức tạp, tôi chỉ tham chiếu đến vòng tròn đơn vị và thêm dấu âm vào mọi thứ. Ví dụ: nếu tôi muốn vị trí bắt đầu ở vị trí π/2 trên vòng tròn đơn vị, tôi sẽ đặt startAngle là -π/2.


-scanAngle: Cách hoạt động của nó là bất kỳ giá trị nào được chỉ định ở đây sẽ được thêm vào startAngle và đó sẽ là nơi các cung kết thúc. Ví dụ, tham chiếu đến vòng tròn đơn vị, nếu chúng ta muốn một cung từ π/2 đến 0, chúng ta sẽ cần startAngle là -π/2 và scanAngle là π/2. -π/2 + π/2 = 0. Yay, lớp toán!


  • Kết quả cuối cùng


Vì vậy, quay lại ví dụ được tham chiếu ở đầu:




Làm thế nào để đi về nó? Chà, để thay đổi các chỉ số tiến độ, chúng ta chỉ cần mày mò

với startAngle và scanAngle mà thôi. Tham chiếu đến vòng tròn đơn vị mà nó trông bắt đầu

ở 2π/3 và kết thúc ở 11π/6. Thật không may, tôi thực sự không thể tìm ra một cách dễ dàng

để tìm ra quétAngle khi sử dụng startAngle tùy chỉnh như thế này. Việc thêm đơn giản

không thực sự luôn hoạt động. Đây là cách tôi tiếp cận nó. Trong góc phần tư I, chúng ta có

đầy đủ tiêu chuẩn nên đó là π/2. Trong góc phần tư II và IV, chúng ta có 2π/6 lát cắt. Vì vậy,

đó là π/6 + π/6 + π/2 là 5π/6. Đây sẽ là đợt quét của chúng tôi. Mã đầy đủ bên dưới:


import 'package:flutter/material.dart';
import 'dart:math' as math;

const Color darkBlue = Color.fromARGB(255, 18, 32, 47);

void main() {
runApp(MyApp());
}

class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData.dark().copyWith(scaffoldBackgroundColor: darkBlue),
debugShowCheckedModeBanner: false,
home: const Scaffold(
body: Center(
child: ProgressIndicatorButton(
startAngle: -2 * math.pi / 3,
endAngle: 5 * math.pi / 6,
),
),
),
);
}
}

class ProgressIndicatorButton extends StatelessWidget {
const ProgressIndicatorButton({
Key? key,
required this.startAngle,
required this.endAngle,
}) : super(key: key);

final double startAngle;
final double endAngle;

@override
Widget build(BuildContext context) {
const buttonSize = 80.0;
const borderWidth = 2.0;

return Stack(
children: [
Container(
width: buttonSize,
height: buttonSize,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(
color: Colors.white,
width: borderWidth,
),
),
),
SizedBox(
width: buttonSize,
height: buttonSize,
child: CustomPaint(
painter: ProgressIndicatorPainter(
width: borderWidth,
startAngle: startAngle,
sweepAngle: endAngle,
),
child: Center(
child: Container(
width: buttonSize - 20.0,
height: buttonSize - 20.0,
decoration: const BoxDecoration(
color: Colors.blue,
shape: BoxShape.circle,
),
child: const Center(
child: Icon(
Icons.done,
size: 30.0,
),
),
),
),
),
),
],
);
}
}

class ProgressIndicatorPainter extends CustomPainter {
const ProgressIndicatorPainter({
required this.width,
required this.startAngle,
required this.sweepAngle,
}) : super();

final double width;
final double startAngle;
final double sweepAngle;

@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = Colors.blue
..strokeCap = StrokeCap.butt
..style = PaintingStyle.stroke
..strokeWidth = width;
final center = Offset(size.width / 2, size.height / 2);
final radius = (size.width / 2) - (width / 2);
canvas.drawArc(
Rect.fromCircle(center: center, radius: radius),
startAngle,
sweepAngle,
false,
paint,
);
}

@override
bool shouldRepaint(CustomPainter oldDelegate) => false;
}

Lưu ý:


Nếu bạn muốn sử dụng độ thay vì radian, nó khá đơn giản. Chỉ cần chấp nhận giá trị vào Custom Painer dưới dạng độ và sau đó chuyển nó thành radian trước khi thêm nó làm đối số cho phương thức drawArc. Tất cả các khái niệm khác đều giống nhau, chỉ là các giá trị thực tế là khác nhau. Đây là mã cuối cùng với các cấp độ được triển khai:


import 'package:flutter/material.dart';
import 'dart:math' as math;

const Color darkBlue = Color.fromARGB(255, 18, 32, 47);

void main() {
runApp(MyApp());
}

class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData.dark().copyWith(scaffoldBackgroundColor: darkBlue),
debugShowCheckedModeBanner: false,
home: const Scaffold(
body: Center(
child: ProgressIndicatorButton(
startAngle: -120,
endAngle: 150,
),
),
),
);
}
}

class ProgressIndicatorButton extends StatelessWidget {
const ProgressIndicatorButton({
Key? key,
required this.startAngle,
required this.endAngle,
}) : super(key: key);

final int startAngle;
final int endAngle;

@override
Widget build(BuildContext context) {
const buttonSize = 80.0;
const borderWidth = 2.0;

return Stack(
children: [
Container(
width: buttonSize,
height: buttonSize,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(
color: Colors.white,
width: borderWidth,
),
),
),
SizedBox(
width: buttonSize,
height: buttonSize,
child: CustomPaint(
painter: ProgressIndicatorPainter(
width: borderWidth,
startAngle: startAngle,
sweepAngle: endAngle,
),
child: Center(
child: Container(
width: buttonSize - 20.0,
height: buttonSize - 20.0,
decoration: const BoxDecoration(
color: Colors.blue,
shape: BoxShape.circle,
),
child: const Center(
child: Icon(
Icons.done,
size: 30.0,
),
),
),
),
),
),
],
);
}
}

class ProgressIndicatorPainter extends CustomPainter {
const ProgressIndicatorPainter({
required this.width,
required this.startAngle,
required this.sweepAngle,
}) : super();

final double width;
final int startAngle;
final int sweepAngle;

@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = Colors.blue
..strokeCap = StrokeCap.butt
..style = PaintingStyle.stroke
..strokeWidth = width;
final startAngleRad = startAngle * (math.pi / 180.0);
final sweepAngleRad = sweepAngle * (math.pi / 180.0);
final center = Offset(size.width / 2, size.height / 2);
final radius = (size.width / 2) - (width / 2);
canvas.drawArc(
Rect.fromCircle(center: center, radius: radius),
startAngleRad,
sweepAngleRad,
false,
paint,
);
}

@override
bool shouldRepaint(CustomPainter oldDelegate) => false;
}

Bonus! Two more examples




Quy trình tương tự. Chúng ta có startAngle là 0 mà chúng ta cũng có thể viết là 0.0 trong

mã. Đối với scanAngle, chúng ta có đầy đủ góc phần tư trong góc phần tư IV cộng với một

lát cắt π/6 trong góc phần tư III. Vì vậy, đó là π/6 + π/2 là 4π/6 sẽ là sweepAngle của chúng ta.




Một chút thay đổi nhưng vẫn là những khái niệm giống nhau. Ở đây, chúng ta có startAngle là -5π/4. Đối với ScanAngle, chúng ta có đầy đủ góc phần tư trong góc phần tư II, sau đó ở góc phần tư III, chúng ta có lát cắt π/6 và lát cắt π/12. π/2 + π/6 + π/12 = 3π/4. Đây sẽ là sweepAngle.


4. Conclusion( Phần kết luận )


Cách tôi tính toán startAngle và sweepAngle có lẽ không phải là lý tưởng nhất nhưng nó hoạt động. Dù sao, tôi hy vọng bạn đã học được điều gì đó và nếu bạn có bất kỳ câu hỏi nào, vui lòng liên hệ trực tiếp với tôi hoặc để lại nhận xét.


Nhận xét

Bài đăng phổ biến từ blog này

Jetpack Compose VS SwiftUI !VS Flutter

  Việc phát triển Android đã trở nên dễ dàng hơn khi các bản cập nhật liên tục đến. Sau bản cập nhật 2020.3.1, rất nhiều thứ đã thay đổi. Nhưng thay đổi chính mà tôi nghĩ hầu hết các nhà phát triển phải chờ đợi là Jetpack Compose cho ứng dụng sản xuất. Và Kotlin là lựa chọn duy nhất cho jetpack Compose, cũng là ngôn ngữ được ưu tiên. Để biết thêm chi tiết hoặc các thay đổi trên Jetpack Compose, bạn có thể truy cập vào https://developer.android.com/jetpack/compose Tương tự, IOS Development cũng cung cấp một tùy chọn để phát triển khai báo, SwiftUI. Trong IDE, không có thay đổi nào do điều này. Nhưng khái niệm gần giống với Jetpack Compose. Thay vì bảng phân cảnh, chúng tôi tạo giao diện người dùng bằng Swift. Để biết thêm chi tiết hoặc các thay đổi trên SwiftUI, hãy truy cập https://developer.apple.com/xcode/swiftui/ Hãy xem cách cả hai hoạt động bằng cách sử dụng một dự án demo. Tôi đã lấy một số ví dụ về số lần chạm tương tự của Flutter. 1. Android Jetpack Compose Chúng tôi có thể tạo

Thiết kế giao diện với DotNetBar (Phần 1)

Đây là phiên bản DotNetBar hỗ trợ C# và Visual Basic https://www.dropbox.com/s/wx80jpvgnlrmtux/DotNetBar.rar  , phiên bản này hỗ trợ giao diện Metro cực kỳ “dễ thương” Các bạn load về và cài đặt, khi cài đặt xong sẽ có source code mẫu của tất cả các control. Để sử dụng được các control của DotNetBar các bạn nhớ add item vào controls box. Thiết kế giao diện với DotNetBar, giao diện sẽ rất đẹp. Link các video hướng dẫn chi tiết cách sử dụng và coding: http://www.devcomponents.com/dotnetbar/movies.aspx Hiện tại DotNetBar có rất nhiều công cụ cực mạnh, trong đó có 3 công cụ dưới đây: DotNetBar for Windows Forms Requires with Visual Studio 2003, 2005, 2008, 2010 or 2012.   DotNetBar for WPF Requires with Visual Studio 2010 or 2012 and Windows Presentation Foundation.   DotNetBar for Silverlight Requires with Visual Studio 2010 or 2012 and Silverlight. Dưới đây là một số hình ảnh về các control trong DotnetBar.   Metro User Interface  controls with Metro Tiles, toolbars, slide panels, forms,

Một số bài tập Winform C#

Một số bài tập: 1. Mô phỏng game đoán số. Luật chơi:         o Đúng số và đúng vị trí   +         o Đúng số mà sai vị trí      ?         o Sai số và sai vị trí          -         . . .         - Kết quả được tạo ngẫu nhiên từ các số có 4 chữ số.         - Các chữ số có giá trị từ 0-6.         - Người chơi có 6 lần đoán. Chương trình tham khảo: 2. In số điện tử Yêu cầu: người dùng nhập vào 1 số ( hoặc 1 chuỗi số) yêu cầu in ra số đó dưới dạng số điện tử. Chương trình tham khảo: 3. Mô phỏng game CARO  (update) 4. Mô phỏng game DÒ MÌN (update)