欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

Flutter利用Canvas繪制精美表盤效果詳解

 更新時(shí)間:2022年03月19日 16:15:44   作者:loongwind  
這篇文章主要介紹了如何利用Flutter中的Canvas繪制一個(gè)精美的表盤效果,文中的實(shí)現(xiàn)步驟講解詳細(xì),快跟隨小編一起動(dòng)手嘗試一下

前言

趁著周末空閑時(shí)間使用 Flutter 的 Canvas制作了一個(gè)精美表盤。

最終實(shí)現(xiàn)的效果還不錯(cuò),如下:

前面說到使用 Canvas 實(shí)現(xiàn)該表盤效果,而在 Flutter 中使用 Canvas 更多的則是繼承 CustomPainter 類實(shí)現(xiàn) paint 方法,然后在 CustomPaint 中使用自定義實(shí)現(xiàn)的 CustomPainter。 比如這里創(chuàng)建的 DialPainter 使用如下:

  @override
  Widget build(BuildContext context) {
    double width = MediaQuery.of(context).size.width;
    return Container(
      color: const Color.fromARGB(255, 35, 36, 38), /// 設(shè)置背景
      child: Center(
        child: CustomPaint( 
          size: Size(width, width),
          painter: DialPainter(),
        ),
      ),
    );
  }

class DialPainter extends CustomPainter{
  @override
  void paint(Canvas canvas, Size size) {
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) {
    return true;
  }
}

之后所有繪制的核心代碼都在 DialPainter 中的 paint 中實(shí)現(xiàn)的,其中 shouldRepaint 是指父控件重新渲染時(shí)是否重新繪制,這里設(shè)置為 true 表示每次都重新繪制。

接下來就看具體實(shí)現(xiàn)代碼,我們將整個(gè)表盤效果的實(shí)現(xiàn)分為三部分:面板刻度、指針。涉及到的主要知識點(diǎn)包括:Paint、Canvas、PathTextPainter 等。

初始化

在開始進(jìn)行繪制之前,先進(jìn)行畫筆和長度單位的初始化。

在整個(gè)效果的實(shí)現(xiàn)上會(huì)多次使用到畫筆 Paint ,為了避免創(chuàng)建多個(gè)畫筆實(shí)例,所以創(chuàng)建一個(gè) Paint 成員變量,后續(xù)通過修改其屬性值來滿足不同效果的繪制。

  late final Paint _paint = _initPaint();

  Paint _initPaint() {
    return Paint()
      ..isAntiAlias = true
      ..color = Colors.white;
  }

通過初始化代碼設(shè)置了畫筆的抗鋸齒和默認(rèn)顏色。

為了方便后續(xù)使用長、寬、半徑等長度,創(chuàng)建對應(yīng)的成員變量,同時(shí)為了適配不同表盤寬高,保證展示效果一致,在繪制時(shí)不直接使用數(shù)值,而使用比例長度:

/// 畫布寬度
late double width;
/// 畫布高度
late double height;
/// 表盤半徑
late double radius;
/// 比例單位長度
late double unit ;

@override
void paint(Canvas canvas, Size size) {
  initSize(size);
}

void initSize(Size size) {
  width = size.width;
  height = size.height;
  radius = min(width, height) / 2;
  unit = radius / 15;
}

半徑取寬度和高度的最小值,然后除以 2 ,單位長度 unit 取值為半徑除以 15。

關(guān)于 Flutter 屏幕適配請參考:Flutter應(yīng)用框架搭建之屏幕適配詳解

面板

首先繪制一個(gè)線性漸變的圓:

/// 繪制一個(gè)線性漸變的圓
var gradient = ui.Gradient.linear(
  Offset(width/2, height/2 - radius,),
  Offset(width/2, height/2 + radius),
  [const Color(0xFFF9F9F9), const Color(0xFF666666)]);

_paint.shader = gradient;
_paint.color = Colors.white;
canvas.drawCircle(Offset(width/2, height/2), radius, _paint);

通過 Gradient.linear 創(chuàng)建一個(gè)線性漸變顏色并設(shè)置給 Paint.shader,繪制出來效果如下:

然后在其上添加一層徑向漸變,增加表盤的立體感:

/// 繪制一層徑向漸變的圓
var radialGradient = ui.Gradient.radial(Offset(width/2, height/2), radius, [
  const Color.fromARGB(216, 246, 248, 249),
  const Color.fromARGB(216, 229, 235, 238),
  const Color.fromARGB(216,205, 212, 217),
  const Color.fromARGB(216,245, 247, 249),
], [0, 0.92, 0.93, 1.0]);

_paint.shader = radialGradient;
canvas.drawCircle(Offset(width/2, height/2), radius -  0.3 * unit, _paint);

使用 Gradient.radial 創(chuàng)建一個(gè)徑向漸變的顏色,效果如下:

最后再在表盤內(nèi)添加一個(gè)邊框和陰影增加對比效果:

/// 繪制 border
var shadowRadius = radius -  0.8 * unit;
_paint
  ..color = const Color.fromARGB(33, 0, 0, 0)
  ..shader = null
  ..style = PaintingStyle.stroke
  ..strokeWidth = 0.1 * unit;
canvas.drawCircle(Offset(width/2, height/2), shadowRadius - 0.2 * unit, _paint);

///繪制陰影
Path path = Path();
path.moveTo(width/2, height/2);
var rect = Rect.fromLTRB(width/2 - shadowRadius, height/2 - shadowRadius, width/2+shadowRadius, height /2 +shadowRadius);
path.addOval(rect);
canvas.drawShadow(path, const Color.fromARGB(51, 0, 0, 0), 1 * unit, true);

最后表盤效果如下:

刻度

面板繪制完成,接下來就是繪制刻度線以及刻度值。

刻度線

代碼如下:

double dialCanvasRadius = radius -  0.8 * unit;
canvas.save();
canvas.translate(width/2, height/2);

var y = 0.0;
var x1 = 0.0;
var x2 = 0.0;

_paint.shader = null;
_paint.color = const Color(0xFF929394);
for( int i = 0; i < 60; i++){
  x1 =  dialCanvasRadius - (i % 5 == 0 ? 0.85 * unit : 1 * unit);
  x2 = dialCanvasRadius - (i % 5 == 0 ? 2 * unit : 1.67 * unit);
  _paint.strokeWidth = i % 5 == 0 ? 0.38 * unit : 0.2 * unit;
  canvas.drawLine(Offset(x1, y), Offset(x2, y), _paint);
  canvas.rotate(2*pi/60);
}
canvas.restore();

表盤上有 60 個(gè)刻度,其中 12 個(gè)為小時(shí)刻度其余為分鐘刻度,循環(huán) 60 次,通過 i % 5 == 0 判斷是否為小時(shí)刻度,從而使用不同的 x 和 y 坐標(biāo),實(shí)現(xiàn)不同的長度和寬度。

這里為了避免去計(jì)算圓上的點(diǎn)坐標(biāo),采用的是旋轉(zhuǎn)畫布來實(shí)現(xiàn)。畫布默認(rèn)旋轉(zhuǎn)點(diǎn)位左上角,所以需要通過 canvas.translate(width/2, height/2) 將旋轉(zhuǎn)點(diǎn)移動(dòng)到表盤的中心點(diǎn),然后每繪制完一個(gè)刻度畫布旋轉(zhuǎn) 2*pi/60 的角度,即 6 度。因?yàn)楫嫴歼M(jìn)行了平移所以繪制的坐標(biāo)都是基于圓中心,即相當(dāng)于圓點(diǎn)移動(dòng)到了圓中心。

最終實(shí)現(xiàn)刻度效果如圖:

刻度值

繪制完刻度后需要給刻度標(biāo)值,這里只顯示 3、6、9、12 四個(gè)刻度值,代碼如下:

double dialCanvasRadius = radius -  0.8 * unit;
var textPainter = TextPainter(
  text: const TextSpan(
    text:
    "3",
    style: TextStyle(color: Colors.black, fontSize: 20, fontWeight: FontWeight.bold, height: 1.0)),
  textDirection: TextDirection.rtl,
  textWidthBasis: TextWidthBasis.longestLine,
  maxLines: 1,
)..layout();

var offset = 2.25 * unit;
var points = [
  Offset(width / 2 + dialCanvasRadius - offset - textPainter.width , height / 2 - textPainter.height / 2),
  Offset(width / 2 - textPainter.width /2, height / 2 + dialCanvasRadius - offset - textPainter.height),
  Offset(width / 2 - dialCanvasRadius + offset, height / 2 - textPainter.height / 2),
  Offset(width / 2 - textPainter.width, height / 2 - dialCanvasRadius + offset),
];
for(int i = 0; i< 4; i++){

  textPainter = TextPainter(
    text: TextSpan(
      text:
      "${(i + 1) * 3}",
      style: const TextStyle(color: Colors.black, fontSize: 20, fontWeight: FontWeight.bold, height: 1.0)),
    textDirection: TextDirection.rtl,
    textWidthBasis: TextWidthBasis.longestLine,
    maxLines: 1,
  )..layout();

  textPainter.paint(canvas, points[i]);
}

繪制文字使用的是 TextPainter 對象,首先創(chuàng)建一個(gè) TextPainter 對象,用于測量獲取文字的寬高,因?yàn)檫@里只顯示 4 個(gè)刻度值,所以這里直接將對應(yīng)需要繪制的坐標(biāo)計(jì)算出來,然后循環(huán)繪制顯示的刻度值在對應(yīng)的位置即可。實(shí)現(xiàn)后效果如下:

指針

接下來就是指針的繪制,指針分為三部分:時(shí)針分針、秒針。在繪制指針之前還需要繪制中心點(diǎn):

var radialGradient =
  ui.Gradient.radial(Offset(width / 2, height / 2), radius, [
    const Color.fromARGB(255, 200, 200, 200),
    const Color.fromARGB(255, 190, 190, 190),
    const Color.fromARGB(255, 130, 130, 130),
  ], [0, 0.9, 1.0]);

/// 底部背景
_paint
  ..shader = radialGradient
  ..style = PaintingStyle.fill;
canvas.drawCircle(
  Offset(width/2, height/2), 2 * unit, _paint);


/// 頂部圓點(diǎn)
_paint
  ..shader = null
  ..style = PaintingStyle.fill
  ..color = const Color(0xFF121314);
canvas.drawCircle(Offset(width/2, height/2), 0.8 * unit, _paint);

代碼很簡單,在中心繪制兩個(gè)圓,一個(gè)底部的徑向漸變的大圓,一個(gè)頂部深色的小圓,如圖:

時(shí)針

時(shí)針分為三部分,連接中心的矩形、連接矩形的半圓弧、最后的箭頭,如圖:

代碼實(shí)現(xiàn)如下:

double hourHalfHeight = 0.4 * unit;
double hourRectRight =   7 * unit;

Path hourPath = Path();
/// 添加矩形 時(shí)針主體
hourPath.moveTo(0 - hourHalfHeight, 0 - hourHalfHeight);
hourPath.lineTo(hourRectRight, 0 - hourHalfHeight);
hourPath.lineTo(hourRectRight, 0 + hourHalfHeight);
hourPath.lineTo(0 - hourHalfHeight, 0 + hourHalfHeight);

/// 時(shí)針箭頭尾部弧形
double offsetTop = 0.5 * unit;
double arcWidth = 1.5 * unit;
double arrowWidth = 2.17 * unit;
double offset = 0.42 * unit;
var rect = Rect.fromLTWH(hourRectRight - offset, 0 - hourHalfHeight - offsetTop, arcWidth, hourHalfHeight * 2 + offsetTop * 2);
hourPath.addArc(rect, pi/2, pi);
/// 時(shí)針箭頭
hourPath.moveTo(hourRectRight - offset + arcWidth/2, 0 - hourHalfHeight - offsetTop);
hourPath.lineTo(hourRectRight - offset + arcWidth/2 + arrowWidth, 0);
hourPath.lineTo(hourRectRight - offset + arcWidth/2, 0 + hourHalfHeight + offsetTop);
hourPath.close();

canvas.save();
canvas.translate(width/2, height/2);
///繪制
_paint.color = const Color(0xFF232425);
canvas.drawPath(hourPath, _paint);
canvas.restore();

這里是通過 Path 先添加一個(gè)矩形到路徑,然后添加一個(gè)圓弧,圓弧向左偏移一定單位,防止對接效果不好,再添加一個(gè)三角形也就是箭頭圖形。這里所有的坐標(biāo)計(jì)算都是基于圓點(diǎn)在圓盤的中心點(diǎn)計(jì)算的,所以需要平移畫布,將圓點(diǎn)移動(dòng)到圓盤的中心點(diǎn),即 canvas.translate(width/2, height/2) 跟繪制表盤刻度的思路是一樣的,最后再通過 canvas.drawPath 進(jìn)行繪制。效果如下:

分針

分針的繪制相對比較簡單,因?yàn)榉轴樉鸵粋€(gè)圓角矩形,使用畫布的 drawRRect 方法即可:

double hourHalfHeight = 0.4 * unit;
double minutesLeft = -1.33 * unit;
double minutesTop = -hourHalfHeight;
double minutesRight = 11* unit;
double minutesBottom = hourHalfHeight;

canvas.save();
canvas.translate(width/2, height/2);

/// 繪制分針
var rRect = RRect.fromLTRBR(minutesLeft, minutesTop, minutesRight, minutesBottom, Radius.circular(0.42 * unit));
_paint.color = const Color(0xFF343536);
canvas.drawRRect(rRect, _paint);

canvas.restore();

實(shí)現(xiàn)思路同樣是將畫布移動(dòng)到圓點(diǎn),然后計(jì)算坐標(biāo)進(jìn)行繪制,這里需要注意的是分針尾部是超過了中心大圓點(diǎn)的,所以這里 left 需要向左偏移一定單位:

這里為了看到分針的效果,將時(shí)針隱藏掉了

秒針

秒針分為四部分:尾部弧形、尾部圓角矩形、細(xì)針、中心圓點(diǎn):

實(shí)現(xiàn)代碼:

double hourHalfHeight = 0.4 * unit;
double secondsLeft = -4.5 * unit;
double secondsTop = -hourHalfHeight;
double secondsRight = 12.5 * unit;
double secondsBottom = hourHalfHeight;

Path secondsPath = Path();
secondsPath.moveTo(secondsLeft, secondsTop);

/// 尾部弧形
var rect = Rect.fromLTWH(secondsLeft, secondsTop, 2.5 * unit, hourHalfHeight * 2);
secondsPath.addArc(rect, pi/2, pi);

/// 尾部圓角矩形
var rRect = RRect.fromLTRBR(secondsLeft + 1 * unit, secondsTop, - 2 * unit, secondsBottom, Radius.circular(0.25 * unit));
secondsPath.addRRect(rRect);

/// 指針
secondsPath.moveTo(- 2 * unit, - 0.125 * unit);
secondsPath.lineTo(secondsRight, 0);
secondsPath.lineTo(-2 * unit, 0.125 * unit);

/// 中心圓
var ovalRect = Rect.fromLTWH(- 0.67 * unit, - 0.67 * unit, 1.33 * unit, 1.33 * unit);
secondsPath.addOval(ovalRect);

canvas.save();
canvas.translate(width/2, height/2);

/// 繪制陰影
canvas.drawShadow(secondsPath, const Color(0xFFcc0000), 0.17 * unit, true);

/// 繪制秒針
_paint.color = const Color(0xFFcc0000);
canvas.drawPath(secondsPath, _paint);

canvas.restore();

思路跟時(shí)針的實(shí)現(xiàn)是一樣的,通過 Path 將圓弧、圓角矩形、三角形、中心圓形組合起來,計(jì)算坐標(biāo)同樣的是以圓盤中心為圓點(diǎn),所有同樣需要使用 translate 移動(dòng)畫布圓點(diǎn)后繪制。實(shí)現(xiàn)效果:

同樣的為了更好的看到秒針的效果,將時(shí)針、分針隱藏了

動(dòng)起來

經(jīng)過上面的繪制,我們將表盤的所有元素都繪制出來了,但是最重要的沒有動(dòng)起來,動(dòng)起來的關(guān)鍵就是要讓時(shí)針、分針、秒針偏移一定的角度,既然是偏移角度自然就想到了旋轉(zhuǎn)畫布來實(shí)現(xiàn),類似于繪制刻度一樣。

分別在時(shí)針、分針、秒針的繪制之前對畫布進(jìn)行一定角度的旋轉(zhuǎn):

/// 時(shí)針
canvas.save();
canvas.translate(width/2, height/2);
canvas.rotate(2*pi/4);
_paint.color = const Color(0xFF232425);
canvas.drawPath(hourPath, _paint);
canvas.restore();

///分針
canvas.save();
canvas.translate(width/2, height/2);
canvas.rotate(2*pi/4*2);
var rRect = RRect.fromLTRBR(minutesLeft, minutesTop, minutesRight, minutesBottom, Radius.circular(0.42 * unit));
_paint.color = const Color(0xFF343536);
canvas.drawRRect(rRect, _paint);
canvas.restore();

///秒針
canvas.save();
canvas.translate(width/2, height/2);
canvas.rotate(2*pi/4*3);
canvas.drawShadow(secondsPath, const Color(0xFFcc0000), 0.17 * unit, true);
_paint.color = const Color(0xFFcc0000);
canvas.drawPath(secondsPath, _paint);
canvas.restore();

分別在時(shí)針、分針、秒針的繪制前對畫布旋轉(zhuǎn) 90°、180°、270° ,效果如下:

通過畫布旋轉(zhuǎn)實(shí)現(xiàn)了我們想要的效果,接下來就是讓指針根據(jù)時(shí)間旋轉(zhuǎn)相應(yīng)的角度。可以通過 DateTime.now() 獲取當(dāng)前時(shí)間對象,進(jìn)而獲取當(dāng)前的小時(shí)、分鐘和秒。然后根據(jù)對應(yīng)的值計(jì)算出相應(yīng)的角度:

 var date = DateTime.now();

/// 時(shí)針
canvas.rotate(2*pi/60*((date.hour - 3 + date.minute / 60 + date.second/60/60) * 5 ));

/// 分針
canvas.rotate(2*pi/60 * (date.minute - 15 + date.second / 60));

/// 秒針
canvas.rotate(2*pi/60 * (date.second - 15));

首先將 360 度分為 60 份,時(shí)針一小時(shí)為 5 份,因?yàn)榻嵌鹊钠鹗际窃谟覀?cè)中心點(diǎn),所以獲取的小時(shí)需要減 3,再加上分鐘、秒鐘占小時(shí)的比例;同理分別計(jì)算分鐘、秒鐘的角度,最終實(shí)現(xiàn)時(shí)針、分針、秒針根據(jù)當(dāng)前時(shí)間展示。

角度計(jì)算對了以后,還需要刷新整個(gè)表盤,即每秒鐘刷新一次,刷新時(shí)獲取當(dāng)前時(shí)間重新繪制時(shí)針、分針、秒針的位置,實(shí)現(xiàn)動(dòng)態(tài)效果,這里使用 Timer 每一秒鐘調(diào)用父布局的 setState 實(shí)現(xiàn)。

  @override
  void initState() {
    super.initState();

    Timer.periodic(const Duration(seconds: 1), (timer) {
      setState(() {});
    });
  }

大功告成,最終實(shí)現(xiàn)了開始展示的表盤動(dòng)態(tài)效果。

以上就是Flutter利用Canvas繪制精美表盤效果詳解的詳細(xì)內(nèi)容,更多關(guān)于Flutter Canvas表盤的資料請關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • Android studio 生成帶Kotlin文檔的實(shí)現(xiàn)方式

    Android studio 生成帶Kotlin文檔的實(shí)現(xiàn)方式

    這篇文章主要介紹了Android studio 生成帶Kotlin文檔的實(shí)現(xiàn)方式,具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧
    2020-03-03
  • 解決ViewPager和SlidingPaneLayout的滑動(dòng)事件沖突問題

    解決ViewPager和SlidingPaneLayout的滑動(dòng)事件沖突問題

    下面小編就為大家分享一篇解決ViewPager和SlidingPaneLayout的滑動(dòng)事件沖突問題,具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧
    2018-01-01
  • Android EditText詳解及示例代碼

    Android EditText詳解及示例代碼

    本文主要介紹Android EditText 的知識,這里整理了詳細(xì)資料及簡單示例代碼有興趣的小伙伴可以參考下
    2016-09-09
  • Android ViewDragHelper使用方法詳解

    Android ViewDragHelper使用方法詳解

    這篇文章主要為大家詳細(xì)介紹了Android ViewDragHelper的使用方法,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2017-12-12
  • Android實(shí)現(xiàn)計(jì)步器功能

    Android實(shí)現(xiàn)計(jì)步器功能

    這篇文章主要為大家詳細(xì)介紹了Android實(shí)現(xiàn)計(jì)步器功能,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2021-03-03
  • Android自定義星星可滑動(dòng)評分控件

    Android自定義星星可滑動(dòng)評分控件

    這篇文章主要介紹了Android自定義星星可滑動(dòng)評分控件,通過線性布局結(jié)合ImageView實(shí)現(xiàn)評分控件,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2018-03-03
  • Android延時(shí)操作的三種方法

    Android延時(shí)操作的三種方法

    這篇文章主要為大家詳細(xì)介紹了Android延時(shí)操作的三種方法,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2017-10-10
  • 詳解Android如何實(shí)現(xiàn)自定義的動(dòng)畫曲線

    詳解Android如何實(shí)現(xiàn)自定義的動(dòng)畫曲線

    最近在寫動(dòng)畫相關(guān)的篇章,經(jīng)常會(huì)用到 Curve 這個(gè)動(dòng)畫曲線類,那這個(gè)類到底怎么實(shí)現(xiàn)的?如果想自己來一個(gè)自定義的動(dòng)畫曲線該怎么弄?本文將為大家詳細(xì)解答
    2022-04-04
  • Android實(shí)現(xiàn)檢測實(shí)體按鍵事件并屏蔽

    Android實(shí)現(xiàn)檢測實(shí)體按鍵事件并屏蔽

    這篇文章主要介紹了Android實(shí)現(xiàn)檢測實(shí)體按鍵事件并屏蔽 ,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2021-08-08
  • Android中獲取狀態(tài)欄高度的兩種方法分享

    Android中獲取狀態(tài)欄高度的兩種方法分享

    在android應(yīng)用中,有時(shí)需要計(jì)算個(gè)View的位置,導(dǎo)致需要計(jì)算狀態(tài)欄高度。為以后方便,在此做個(gè)簡單記錄。下面這篇文章主要介紹了Android中獲取狀態(tài)欄高度的兩種方法,兩種方法分別給出了示例代碼,有需要的朋友可以參考借鑒。
    2017-02-02

最新評論