Android Flutter實(shí)現(xiàn)點(diǎn)贊效果的示例代碼
前言
點(diǎn)贊這個動作不得不說在社交、短視頻等App中實(shí)在是太常見了,當(dāng)用戶手指按下去的那一刻,給用戶一個好的反饋效果也是非常重要的,這樣用戶點(diǎn)起贊來才會有一種強(qiáng)烈的我點(diǎn)了贊的效果,那么今天我們就用Flutter實(shí)現(xiàn)一個掘金App上的點(diǎn)贊效果。
首先我們看下掘金App的點(diǎn)贊組成部分,有一個小手,點(diǎn)贊數(shù)字、點(diǎn)贊氣泡效果,還有一個震動反饋,接下來我們一步一步實(shí)現(xiàn)。
知識點(diǎn):繪制、動畫、震動反饋
繪制小手
這里我們使用Flutter的Icon圖標(biāo)中的點(diǎn)贊小手,Icons圖標(biāo)庫為我們提供了很多App常見的小圖標(biāo),如果使用蘋果蘋果風(fēng)格的小圖標(biāo)可以使用cupertino_icons: ^1.0.2
插件,圖標(biāo)并不是圖片,本質(zhì)上和emoji
圖標(biāo)一樣,可以添加到文本中使用,所以圖標(biāo)才可以設(shè)置不同的顏色屬性,對比使用png格式圖標(biāo)可以節(jié)省不少的內(nèi)存。
接下來我們就將這兩個圖標(biāo)繪制出來,首先我們從上圖可以看到真正的圖標(biāo)數(shù)據(jù)其實(shí)是IconData
類,里面有一個codePoint
屬性可以獲取到Unicode
統(tǒng)一碼,通過String.fromCharCode(int charCode)
可以返回一個代碼單元,在Text文本中支持顯示。
class IconData{ /// The Unicode code point at which this icon is stored in the icon font. /// 獲取此圖標(biāo)的Unicode代碼點(diǎn) final int codePoint; } class String{ /// 如果[charCode]可以用一個UTF-16編碼單元表示,則新的字符串包含一個代碼單元 external factory String.fromCharCode(int charCode); }
接下來我們就可以把圖標(biāo)以繪制文本的形式繪制出來了
關(guān)鍵代碼:
// 贊圖標(biāo) final icon = Icons.thumb_up_alt_outlined; // 通過TextPainter可以獲取圖標(biāo)的尺寸 TextPainter textPainter = TextPainter( text: TextSpan( text: String.fromCharCode(icon.codePoint), style: TextStyle( fontSize: 30, fontFamily: icon.fontFamily,// 字體形象家族,這個字段一定要設(shè)置,不然顯示不出來 color: Colors.black)), textAlign: TextAlign.center, textDirection: TextDirection.ltr); textPainter.layout(); // 進(jìn)行布局 Size size2 = textPainter.size; // 尺寸必須在布局后獲取 //將圖標(biāo)偏移到畫布中央 textPainter.paint(canvas, Offset(-size2.width / 2, -size2.height / 2));
通過上方代碼我們就實(shí)現(xiàn)了將圖標(biāo)繪制到畫板當(dāng)中
接下來繼續(xù)繪制點(diǎn)贊數(shù)量
代碼:
TextPainter textPainter2 = TextPainter( text: TextSpan( text: "點(diǎn)贊",// 點(diǎn)贊數(shù)量 style: TextStyle( fontSize: 9, fontWeight: FontWeight.w500, color: Colors.black)), textAlign: TextAlign.center, textDirection: TextDirection.ltr); textPainter2.layout(); // 進(jìn)行布局 // 向右上進(jìn)行偏移在小手上面 textPainter2.paint(canvas, Offset(size.width / 9, -size.height / 2 + 5));
然后圖標(biāo)就變成了這樣樣子
我們看到,掘金App點(diǎn)贊的過程中,周圍還有一些小氣泡的效果,這里提供一個思路,將這些氣泡的坐標(biāo)點(diǎn)放到一個圓的外環(huán)上面,通過動畫改變圓的半徑達(dá)到小圓點(diǎn)由內(nèi)向外發(fā)散,發(fā)散的同時改變小圓點(diǎn)的大小,從而達(dá)到氣泡的效果, 關(guān)鍵代碼:
var r = size.width / 2 - 15; // 半徑 var d = 4; // 偏移量 氣泡的移動距離 // 繪制小圓點(diǎn) 一共4個 掘金也是4個 角度可以自由發(fā)揮 這里根據(jù)掘金App的發(fā)散角度定義的 canvas.drawPoints( ui.PointMode.points, [ Offset((r + d * animation2.value) * cos(pi - pi / 18 * 2), (r + d * animation2.value) * sin(pi - pi / 18 * 2)), Offset((r + d * animation2.value) * cos(pi + pi / 18 * 2), (r + d * animation2.value) * sin(pi + pi / 18 * 2)), Offset((r + d * animation2.value) * cos(pi * 1.5 - pi / 18), (r + d * animation2.value) * sin(pi * 1.5 - pi / 18)), Offset((r + d * animation2.value) * cos(pi * 1.5 + pi / 18 * 5), (r + d * animation2.value) * sin(pi * 1.5 + pi / 18 * 5)), ], _paint ..strokeWidth = 5 ..color = Colors.blue ..strokeCap = StrokeCap.round);
得到現(xiàn)在的圖形, 發(fā)散前
發(fā)散后
接下來繼續(xù)我們來添加交互效果,添加動畫,如果有看上一篇吃豆人,相信這里就很so easy了,首先創(chuàng)建兩個動畫類,控制小手和氣泡,再創(chuàng)建兩個變量,是否點(diǎn)贊和點(diǎn)贊數(shù)量,代碼:
late Animation<double> animation; // 贊 late Animation<double> animation2; // 小圓點(diǎn) ValueNotifier<bool> isZan = ValueNotifier(false); // 記錄點(diǎn)贊狀態(tài) 默認(rèn)沒點(diǎn)贊 ValueNotifier<int> zanNum = ValueNotifier(0); // 記錄點(diǎn)贊數(shù)量 默認(rèn)0點(diǎn)贊
這里我們需要使用動畫曲線CurvedAnimation
這個類,這個類可以實(shí)現(xiàn)不同的0-1的運(yùn)動曲線,根據(jù)掘金的點(diǎn)贊效果,比較符合這個曲線規(guī)則,快速放大,然后回歸正常大小,這個類幫我們實(shí)現(xiàn)了很多好玩的運(yùn)動曲線,有興趣的小伙伴可以嘗試下其他運(yùn)動曲線。
小手運(yùn)動曲線:
氣泡運(yùn)動曲線:
有了運(yùn)動曲線之后,接下來我們只需將屬性賦值給小手手和小圓點(diǎn)就好了
完整源碼
封裝一下,對外暴露大小,就是一個點(diǎn)贊組件了。
class ZanDemo extends StatefulWidget { const ZanDemo({Key? key}) : super(key: key); @override _ZanDemoState createState() => _ZanDemoState(); } class _ZanDemoState extends State<ZanDemo> with TickerProviderStateMixin { late Animation<double> animation; // 贊 late Animation<double> animation2; // 小圓點(diǎn) ValueNotifier<bool> isZan = ValueNotifier(false); // 記錄點(diǎn)贊狀態(tài) 默認(rèn)沒點(diǎn)贊 ValueNotifier<int> zanNum = ValueNotifier(0); // 記錄點(diǎn)贊數(shù)量 默認(rèn)0點(diǎn)贊 late AnimationController _controller; // 控制器 late AnimationController _controller2; // 小圓點(diǎn)控制器 late CurvedAnimation cure; // 動畫運(yùn)行的速度軌跡 速度的變化 late CurvedAnimation cure2; // 動畫運(yùn)行的速度軌跡 速度的變化 int time = 0;// 防止快速點(diǎn)兩次贊導(dǎo)致取消贊 @override void initState() { super.initState(); _controller = AnimationController( vsync: this, duration: const Duration(milliseconds: 500)); //500ms _controller2 = AnimationController( vsync: this, duration: const Duration(milliseconds: 500)); //500ms cure = CurvedAnimation(parent: _controller, curve: Curves.easeInOutBack); cure2 = CurvedAnimation(parent: _controller2, curve: Curves.easeOutQuint); animation = Tween(begin: 0.0, end: 1.0).animate(cure); animation2 = Tween(begin: 0.0, end: 1.0).animate(_controller2); } @override Widget build(BuildContext context) { return InkWell( child: Center( child: CustomPaint( size: Size(50, 50), painter: _ZanPainter(animation, animation2, isZan, zanNum, Listenable.merge([animation, animation2, isZan, zanNum])), ), ), onTap: () { if (!isZan.value && !_isDoubleClick()) { _controller.forward(from: 0); // 延遲300ms彈窗氣泡 Timer(Duration(milliseconds: 300), () { isZan.value = true; _controller2.forward(from: 0); }); Vibrate.feedback(FeedbackType.success); zanNum.value++; } else if (isZan.value) { Vibrate.feedback(FeedbackType.success); isZan.value = false; zanNum.value--; } }, ); } bool _isDoubleClick() { if (time == 0) { time = DateTime.now().microsecondsSinceEpoch; return false; } else { if (DateTime.now().microsecondsSinceEpoch - time < 800 * 1000) { return true; } else { time = DateTime.now().microsecondsSinceEpoch; return false; } } } } class _ZanPainter extends CustomPainter { Animation<double> animation; Animation<double> animation2; ValueNotifier<bool> isZan; ValueNotifier<int> zanNum; Listenable listenable; _ZanPainter( this.animation, this.animation2, this.isZan, this.zanNum, this.listenable) : super(repaint: listenable); Paint _paint = Paint()..color = Colors.blue; List<Offset> points = []; @override void paint(Canvas canvas, Size size) { canvas.clipRect(Offset.zero & size); canvas.translate(size.width / 2, size.height / 2); // 贊 final icon = isZan.value ? Icons.thumb_up_alt_rounded : Icons.thumb_up_alt_outlined; // 通過TextPainter可以獲取圖標(biāo)的尺寸 TextPainter textPainter = TextPainter( text: TextSpan( text: String.fromCharCode(icon.codePoint), style: TextStyle( fontSize: animation.value < 0 ? 0 : animation.value * 30, fontFamily: icon.fontFamily, color: isZan.value ? Colors.blue : Colors.black)), textAlign: TextAlign.center, textDirection: TextDirection.ltr); textPainter.layout(); // 進(jìn)行布局 Size size2 = textPainter.size; // 尺寸必須在布局后獲取 //將圖標(biāo)偏移到畫布中央 textPainter.paint(canvas, Offset(-size2.width / 2, -size2.height / 2)); var r = size.width / 2 - 15; // 半徑 var d = 4; // 偏移量 canvas.drawPoints( ui.PointMode.points, [ Offset((r + d * animation2.value) * cos(pi - pi / 18 * 2), (r + d * animation2.value) * sin(pi - pi / 18 * 2)), Offset((r + d * animation2.value) * cos(pi + pi / 18 * 2), (r + d * animation2.value) * sin(pi + pi / 18 * 2)), Offset((r + d * animation2.value) * cos(pi * 1.5 - pi / 18 * 1), (r + d * animation2.value) * sin(pi * 1.5 - pi / 18 * 1)), Offset((r + d * animation2.value) * cos(pi * 1.5 + pi / 18 * 5), (r + d * animation2.value) * sin(pi * 1.5 + pi / 18 * 5)), ], _paint ..strokeWidth = animation2.value < 1 ? 5 * animation2.value : 0 ..color = Colors.blue ..strokeCap = StrokeCap.round); TextPainter textPainter2 = TextPainter( text: TextSpan( text: zanNum.value == 0 ? "點(diǎn)贊" : zanNum.value.toString(), style: TextStyle( fontSize: 9, fontWeight: FontWeight.w500, color: Colors.black)), textAlign: TextAlign.center, textDirection: TextDirection.ltr); textPainter2.layout(); // 進(jìn)行布局 // 向右上進(jìn)行偏移在小手上面 textPainter2.paint(canvas, Offset(size.width / 9, -size.height / 2 + 5)); } @override bool shouldRepaint(covariant _ZanPainter oldDelegate) { return oldDelegate.listenable != listenable; } }
到這里發(fā)現(xiàn)是不是少了點(diǎn)什么,不錯,還少了震動的效果,這里我們引入flutter_vibrate: ^1.3.0
這個插件,這個插件是用來管理設(shè)備震動效果的,Andoroid端記得加入震動權(quán)限
<uses-permission android:name="android.permission.VIBRATE"/>
使用方法也很簡單,這個插件封裝了一些常見的提示震動,比如操作成功、操作警告、操作失敗等,其實(shí)就是震動時間的長短,這里我們就在點(diǎn)贊時候調(diào)用Vibrate.feedback(FeedbackType.success);
有一個點(diǎn)擊成功的震動就好了。
最后來看下最終效果圖吧:
是不是和掘金App的效果一樣,不信你點(diǎn)個贊看看~~
以上就是Android Flutter實(shí)現(xiàn)點(diǎn)贊效果的示例代碼的詳細(xì)內(nèi)容,更多關(guān)于Flutter點(diǎn)贊的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Android本地數(shù)據(jù)存儲Room實(shí)踐和優(yōu)化技巧
本文詳細(xì)介紹了Android本地數(shù)據(jù)存儲框架Room的使用,包括基本概念、核心組件、最佳實(shí)踐、優(yōu)化技巧等,幫助開發(fā)者學(xué)習(xí)和掌握Room的使用方法,提升數(shù)據(jù)存儲效率和應(yīng)用性能2023-04-04Android Service(不和用戶交互應(yīng)用組件)案例分析
Service是在一段不定的時間運(yùn)行在后臺,不和用戶交互應(yīng)用組件,本文將詳細(xì)介紹,需要了解的朋友可以參考下2012-12-12Android創(chuàng)建簡單發(fā)送和接收短信應(yīng)用
收發(fā)短信應(yīng)該是每個手機(jī)最基本的功能之一了,即使是許多年前的老手機(jī)也都會具備這項功能,而Android 作為出色的智能手機(jī)操作系統(tǒng),自然也少不了在這方面的支持。今天我們開始自己創(chuàng)建一個簡單的發(fā)送和接收短信的應(yīng)用,需要的朋友可以參考下2016-04-04Android中使用RecyclerView實(shí)現(xiàn)下拉刷新和上拉加載
RecyclerView 是Android L版本中新添加的一個用來取代ListView的SDK,它的靈活性與可替代性比listview更好。這篇文章主要介紹了Android中使用RecyclerView實(shí)現(xiàn)下拉刷新和上拉加載的相關(guān)資料,需要的朋友可以參考下2016-03-03Android 三級NestedScroll嵌套滾動實(shí)踐
這篇文章主要介紹了Android 三級NestedScroll嵌套滾動實(shí)踐,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-02-02基于Retrofit2+RxJava2實(shí)現(xiàn)Android App自動更新
這篇文章主要為大家詳細(xì)介紹了基于Retrofit2+RxJava2實(shí)現(xiàn)Android App自動更新,具有一定的參考價值,感興趣的小伙伴們可以參考一下2018-05-05Android RecyclerView區(qū)分視圖類型的Divider的實(shí)現(xiàn)
本篇文章主要介紹了Android RecyclerView區(qū)分視圖類型的Divider的實(shí)現(xiàn),小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-04-04