<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
前面寫了一篇Flutter利用Canvas繪製精美錶盤效果詳解的文章,對 Flutter 中的 Canvas 使用有了進一步的理解,就想著再用 Canvas 實現一個什麼樣的效果來加深一下對 Canvas 使用的理解,這個時候正好看到群裡有人發紅包,於是就想著能不能在 Flutter 中使用 Canvas 實現微信領取紅包的效果?想到就做,知行合一,經過幾天空餘時間的研究,最終實現了微信領取紅包效果,於是有了這篇文章。
最終實現的整體效果如下:
看完效果以後,接下來就帶領大家來看看是怎樣一步一步實現最終效果的,在正式動手寫程式碼之前,先對整個效果做一個簡單的拆分,將其分為五個部分:
拆分後如下圖所示:
接下來就一步一步來實現。
紅包彈出主要分為兩部分:從小到大縮放動畫、半透明遮罩。很自然的想到了使用 Dialog 來實現,最終也確實使用 Dialog 實現了對應的效果,但是在最後展示結果頁的時候出現問題了,因為紅包開啟與結果展示是同時進行的,結果頁在紅包下面,使用 Dialog 的話會存在結果頁在 Dialog 上面遮住紅包的效果,最後使用了 Overlay
在頂層新增一個 Widget 來實現。
建立一個 RedPacket
的 Widget:
class RedPacket extends StatelessWidget { const RedPacket({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return Center( child: Container( width: 0.8.sw, height: 1.2.sw, color: Colors.redAccent, ) ); } }
內容很簡單,就是一個居中的寬高分別為 0.8.sw
、1.2.sw
的 Container,顏色為紅色。這裡 sw
是代表螢幕寬度,即紅包寬度為螢幕寬度的 0.8 倍,高度為螢幕寬度的 1.2 倍。
關於 Flutter 螢幕適配,請參閱:Flutter應用框架搭建之螢幕適配詳解
然後點選按鈕時通過 Overlay
展示出來, 建立一個 showRedPacket
的方法:
void showRedPacket(BuildContext context){ OverlayEntry entry = OverlayEntry(builder: (context) => RedPacket()); Overlay.of(context)?.insert(entry); }
效果如下:
紅包是彈出來了,但因為沒有縮放動畫,很突兀。為了實現縮放動畫,在 Container 上包裹 ScaleTransition
用於縮放動畫,同時將 RedPacket
改為 StatefulWidget
,因為使用動畫需要用到 AnimationController
傳入 SingleTickerProviderStateMixin
,實現如下:
class RedPacket extends StatefulWidget { const RedPacket({Key? key}) : super(key: key); @override State<RedPacket> createState() => _RedPacketState(); } class _RedPacketState extends State<RedPacket> with SingleTickerProviderStateMixin { late AnimationController scaleController = AnimationController(vsync: this) ..duration = const Duration(milliseconds: 500) ..forward(); @override Widget build(BuildContext context) { return Container( color: Color(0x88000000), /// 半透明遮罩 child: Center( child: ScaleTransition( scale: Tween<double>(begin: 0.0, end: 1.0).animate(CurvedAnimation(parent: scaleController, curve: Curves.fastOutSlowIn)), child: Container( width: 0.8.sw, height: 1.2.sw, color: Colors.redAccent, ), ) ), ); } }
ScaleTransition
設定動畫從 0.0 到 1.0 即從無到原本大小,動畫時間為 500 毫秒;同時在外層再包裹一層 Container 併為其新增半透明顏色實現半透明遮罩,最終實現效果:
這樣就實現了第一部分的功能。
標題說了是使用 Canvas 來實現,所以紅包佈局主要是使用 Canvas 來實現,將前面紅包的 Container 換成 CustomPaint
, 然後建立 RedPacketPainter
繼承自 CustomPainter
:
ScaleTransition( scale: Tween<double>(begin: 0.0, end: 1.0).animate(CurvedAnimation(parent: scaleController, curve: Curves.fastOutSlowIn)), child: CustomPaint( size: Size(1.sw, 1.sh), painter: RedPacketPainter(), ), )
考慮到後續動畫,這裡將畫布的大小設定為全螢幕。紅包佈局的核心程式碼就在 RedPacketPainter
裡,首先繪製紅包的背景,背景分為上下兩部分,上部分又由一個矩形和一個圓弧組成,下半部分同樣是由一個矩形和一個圓弧組成,上半部分的圓弧是凸出來的,而下半部分的是凹進去的,示意圖如下:
初始化:
/// 畫筆 late final Paint _paint = Paint()..isAntiAlias = true; /// 路徑 final Path path = Path(); /// 紅包的高度:1.2倍的螢幕寬度 late double height = 1.2.sw; /// 上半部分貝塞爾曲線的結束點 late double topBezierEnd = (1.sh - height)/2 + height/8*7; /// 上半部分貝塞爾曲線的起點 late double topBezierStart= topBezierEnd - 0.2.sw; /// 下半部分貝塞爾曲線的起點 late double bottomBezierStart = topBezierEnd - 0.4.sw; /// 金幣中心點,後續通過path計算 Offset goldCenter = Offset.zero; /// 橫向的中心點 final double centerWidth = 0.5.sw; /// 紅包在整個介面的left late double left = 0.1.sw; /// 紅包在整個介面的right late double right = 0.9.sw; /// 紅包在整個介面的top late double top = (1.sh - height)/2; /// 紅包在整個介面的bottom late double bottom = (1.sh - height)/2 + height;
程式碼實現如下:
void drawTop(ui.Canvas canvas) { path.reset(); path.addRRect(RRect.fromLTRBAndCorners(left, top, right, topBezierStart, topLeft: const Radius.circular(5), topRight: const Radius.circular(5))); var bezierPath = getTopBezierPath(); path.addPath(bezierPath, Offset.zero); path.close(); canvas.drawShadow(path, Colors.redAccent, 2, true); canvas.drawPath(path, _paint); }
這裡使用 Path
來進行繪製,首先向路徑中新增一個圓角矩形,也就是示意圖中的第①部分,然後通過 getTopBezierPath
獲取一個貝塞爾曲線的 bezierPath 並將其新增到 path 路徑中,getTopBezierPath
原始碼如下:
Path getTopBezierPath() { Path bezierPath = Path(); bezierPath.moveTo(left, topBezierStart); bezierPath.quadraticBezierTo(centerWidth, topBezierEnd, right , topBezierStart); var pms = bezierPath.computeMetrics(); var pm = pms.first; goldCenter = pm.getTangentForOffset(pm.length / 2)?.position ?? Offset.zero; return bezierPath; }
getTopBezierPath 原始碼分為兩部分,第一部分是建立貝塞爾曲線的 path ,使用的是最開始初始化的資料建立,實現示意圖中的第②部分內容;然後根據建立好的貝塞爾曲線的 path 計算出路徑中中間點的座標,作為金幣中心點座標。示意圖如下:
圖中紅點就是貝塞爾曲線的點,中間實線就是貝塞爾曲線,也就是上面程式碼中建立的貝塞爾曲線路徑,實線中間的點就是金幣位置的中心點。
貝塞爾曲線繪製完成後呼叫 drawShadow
繪製陰影,作用是突出上下兩部分連線處的效果,最後通過 path 繪製出整個上半部分的效果,如下:
程式碼實現如下:
void drawBottom(ui.Canvas canvas) { path.reset(); path.moveTo(left, bottomBezierStart ); path.quadraticBezierTo(centerWidth, topBezierEnd, right , bottomBezierStart); path.lineTo(right, topBezierEnd); path.lineTo(left, topBezierEnd); path.addRRect(RRect.fromLTRBAndCorners(left, topBezierEnd, right, bottom, bottomLeft: const Radius.circular(5), bottomRight: const Radius.circular(5))); path.close(); canvas.drawShadow(path, Colors.redAccent, 2, true); canvas.drawPath(path, _paint); }
下半部分實現同樣分為兩部分,首先繪製出貝塞爾曲線,即示意圖第③部分,然後再新增一個圓角矩形,即示意圖第④部分;然後繪製下半部分的陰影和圖形,單獨展示下半部分效果如下:
將上下兩部分結合起來,就實現了紅包背景的效果,如下:
背景繪製完成後接下來進行金幣的繪製,前面已計算出金幣的中心點座標,靜態金幣的繪製就相對來說比較簡單了,就是一個圓形,程式碼如下:
void drawGold(ui.Canvas canvas){ Path path = Path(); canvas.save(); canvas.translate(0.5.sw, goldCenter.dy); _paint.style = PaintingStyle.fill; path.addOval(Rect.fromLTRB(-40.w , -40.w, 40.w , 40.w)); _paint.color = const Color(0xFFFCE5BF); canvas.drawPath(path, _paint); canvas.restore(); }
這裡將畫布移動到到金幣的中心點,然後向 Path 中新增新增一個半徑為 40.w 的圓,最後將 path 繪製出來即可。效果如下:
金幣繪製出來後,還需在金幣上繪製一個繁體的 "開" 字,程式碼如下:
void drawOpenText(ui.Canvas canvas) { if(controller.showOpenText){ TextPainter textPainter = TextPainter( text: TextSpan( text: "開", style: TextStyle(fontSize: 34.sp, color: Colors.black87, height: 1.0, fontWeight: FontWeight.w400) ), textDirection: TextDirection.ltr, maxLines: 1, textWidthBasis: TextWidthBasis.longestLine, textHeightBehavior: const TextHeightBehavior(applyHeightToFirstAscent: false, applyHeightToLastDescent: false) )..layout(); canvas.save(); canvas.translate(0.5.sw, goldCenter.dy); textPainter.paint(canvas, Offset(- textPainter.width / 2, -textPainter.height/2)); canvas.restore(); } }
使用 TextPainter 進行文字的繪製,同樣是將畫布移動到金幣的中心,然後繪製文字,效果如下:
經過上面的繪製,效果已經出來了,但是還差紅包封面上的使用者頭像相關文字,使用 Canvas 同樣能實現,但這裡並沒有使用 Canvas 來實現,而是使用 CoustomPaint
的 child 來實現:
CustomPaint( size: Size(1.sw, 1.sh), painter: RedPacketPainter(controller: controller), child: buildChild(), ) Container buildChild() { return Container( padding: EdgeInsets.only(top: 0.3.sh), child: Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ Row( mainAxisAlignment: MainAxisAlignment.center, children: [ ClipRRect( borderRadius: BorderRadius.circular(3.w), child: Image.network("https://p26-passport.byteacctimg.com/img/user-avatar/32f1f514b874554f69fe265644ca84e4~300x300.image", width: 24.w,)), SizedBox(width: 5.w,), Text("loongwind發出的紅包", style: TextStyle(fontSize: 16.sp, color: Color( 0xFFF8E7CB), fontWeight: FontWeight.w500),) ], ), SizedBox(height: 15.w,), Text("恭喜發財", style: TextStyle(fontSize: 18.sp, color: Color( 0xFFF8E7CB)),) ], ), ); }
CoustomPaint
的 child 允許傳入一個 Widget,Widget 的實現就是要顯示的頭像和文字,程式碼如上,最終效果如下:
至此紅包的整個佈局的實現就完成了。
前面完成了紅包的靜態顯示,接下來就看看怎麼讓紅包動起來,首先看看怎麼讓金幣旋轉起來。
說到旋轉首先想到的就是以金幣的中心旋轉,可以通過旋轉畫布的旋轉或者 path 的 transform
旋轉來實現,但是經過實驗使用這種方式能讓金幣旋轉起來,但是做到旋轉的立體效果卻很複雜。所以最終採用的是使用兩個圓在 x 軸上進行一定的偏移,然後壓縮圓的寬度來模擬實現旋轉效果,示意圖如下:
如圖所示,繪製兩個相同的圓,開始時將兩個圓重疊在一起,然後同時壓縮圓的寬度並將下層的圓向左偏移一定單位,就形成了旋轉的立體效果。
程式碼:
void drawGold(ui.Canvas canvas){ Path path = Path(); canvas.save(); canvas.translate(0.5.sw, goldCenter.dy); _paint.style = PaintingStyle.fill; path.addOval(Rect.fromLTRB(-40.w , -40.w, 40.w , 40.w)); _paint.color = const Color(0xFFE5CDA8); canvas.drawPath(path, _paint); _paint.color = const Color(0xFFFCE5BF); canvas.drawPath(path, _paint); canvas.restore(); }
修改上面繪製金幣的程式碼,設定不同的顏色再繪製一個圓,這樣就在同一個位置繪製了兩個不同顏色的圓。那麼怎麼讓它動起來呢?可以使用動畫,通過動畫執行寬度的縮放,是寬度係數從 1 縮放到 0 再從 0 回到 1。因為 CustomPainter
是繼承自 Listenable
,而動畫也是 Listenable
所以直接將動畫與 CustomPainter
結合起來使用更方便。
為了方便統一控制紅包的動畫,建立一個 RedPacketController
,並在裡面建立一個控制金幣旋轉的動畫及控制器:
class RedPacketController{ final SingleTickerProviderStateMixin tickerProvider; late AnimationController angleController; late Animation<double> angleCtrl; RedPacketController({required this.tickerProvider}){ initAnimation(); } void initAnimation() { angleController = AnimationController( duration: const Duration(seconds: 3), vsync: tickerProvider )..repeat(reverse: true); angleCtrl = angleController.drive(Tween(begin: 1.0, end: 0.0)); } void dispose(){ angleController.dispose(); timer?.cancel(); } }
為了看到旋轉的效果,將動畫執行時間設定為 3 秒,並且讓其重複執行,重複執行時設定 reverse 為 true,即反向執行,然後改造 _RedPacketState 混入 SingleTickerProviderStateMixin 並建立 RedPacketController :
class _RedPacketState extends State<RedPacket> with SingleTickerProviderStateMixin{ late RedPacketController controller = RedPacketController(tickerProvider: this); Widget buildRedPacket() { return GestureDetector( onTapUp: controller.clickGold, child: CustomPaint( size: Size(1.sw, 1.sh), painter: RedPacketPainter(controller: controller), child: buildChild(), ), ); } /// ... } class RedPacketPainter extends CustomPainter{ RedPacketController controller; RedPacketPainter({required this.controller}) : super(repaint:controller.angleController); /// ... }
RedPacketPainter 的構造方法呼叫了 super 並傳入了 repaint 引數,即建立的動畫控制器。
這樣就能在繪製金幣的時候使用動畫的值了:
void drawGold(ui.Canvas canvas){ Path path = Path(); double angle = controller.angleCtrl.value; canvas.save(); canvas.translate(0.5.sw, goldCenter.dy); _paint.style = PaintingStyle.fill; path.addOval(Rect.fromLTRB(-40.w * angle , -40.w, 40.w * angle, 40.w)); _paint.color = const Color(0xFFE5CDA8); canvas.drawPath(path, _paint); _paint.color = const Color(0xFFFCE5BF); canvas.drawPath(path, _paint); canvas.restore(); }
通過 controller.angleCtrl.value
獲取當前動畫的值,然後在圓的 left 和 right 引數上乘以這個值,看一下效果:
效果已經有了,但是發現在旋轉到最小的時候中間是空的,這不符合我們的預期,那怎麼辦呢?將兩個圓的邊一一連線起來是不是中間就不空了,如圖所示:
程式碼實現:
void drawGoldCenterRect(ui.Path path, ui.Path path2, ui.Canvas canvas) { var pms1 = path.computeMetrics(); var pms2 = path2.computeMetrics(); var pathMetric1 = pms1.first; var pathMetric2 = pms2.first; var length = pathMetric1.length; Path centerPath = Path(); for(int i = 0; i <length; i++){ var position1 = pathMetric1.getTangentForOffset(i.toDouble())?.position; var position2 = pathMetric2.getTangentForOffset(i.toDouble())?.position; if(position1 == null || position2 == null){ continue; } centerPath.moveTo(position1.dx, position1.dy); centerPath.lineTo(position2.dx, position2.dy); } Paint centerPaint = Paint()..color = const Color(0xFFE5CDA8) ..style = PaintingStyle.stroke..strokeWidth=1; canvas.drawPath(centerPath, centerPaint); }
使用 Path.computeMetrics 計算兩個 path 的路徑點,迴圈路徑每一個點,將兩個 path 的每一個點連線起來然後繪製出來,再來看一下效果:
效果好多了,但是仔細觀察發現還是有一個問題,金幣看著不是旋轉的而是左右搖擺的,這是因為實現的立體的效果一直在一邊導致的,需要根據旋轉的時機將立體效果的方向切換,從 1 到 0 時在右邊,從 0 到 1 時在左邊,通過動畫的狀態進行判斷,修改程式碼如下:
var frontOffset = 0.0; var backOffset = 0.0; if(controller.angleCtrl.status == AnimationStatus.reverse){ frontOffset = 4.w; backOffset = -4.w; }else if(controller.angleCtrl.status == AnimationStatus.forward){ frontOffset = -4.w; backOffset = 4.w; } var path2 = path.shift(Offset(backOffset * (1 - angle), 0)); path = path.shift(Offset(frontOffset * (1 - angle), 0));
再來看一下效果:
這樣旋轉效果就很完美了,金幣中間還缺一個空心的矩形,實現很簡單,在圓的 path 路徑中疊加一個矩形即可:
path.addRect(Rect.fromLTRB(-10.w * angle , -10.w, 10.w * angle , 10.w)); path.fillType = PathFillType.evenOdd;
設定fillType
為 evenOdd
,這樣就形成了中心空心的效果,並且由於上面連線兩個圓的路徑點,這個空心也自帶了立體效果,如圖:
最後為金幣新增點選事件,點選時開啟旋轉,並隱藏金幣上的文字。點選事件可以直接給 CustomPaint 包裹一個 GestureDetector ,點選時判斷點選座標是否在金幣的繪製範圍內,可以使用 Path.contains
進行判斷,所以需要儲存金幣的 path 用於點選判斷,這裡將其儲存到 controller 裡:
controller.goldPath = path.shift(Offset(0.5.sw, goldCenter.dy));
然後在 RedPacketController 裡定義對應的變數和方法:
class RedPacketController{ Path? goldPath; bool showOpenText = true; bool checkClickGold(Offset point){ return goldPath?.contains(point) == true; } void clickGold(TapUpDetails details) { if(checkClickGold(details.globalPosition)){ angleController.repeat(reverse: true); tickerProvider.setState(() { showOpenText = false; }); } } /// ... }
goldPath 用於儲存金幣的 path,showOpenText 用於是否顯示金幣上的文字,點選時判斷事件觸發點是否在金幣範圍內,在金幣範圍內則觸發動畫啟動,並設定金幣上的文字不顯示。
為 CustomPaint 新增點選事件:
Widget buildRedPacket() { return GestureDetector( onTapUp: controller.clickGold, child: CustomPaint( size: Size(1.sw, 1.sh), painter: RedPacketPainter(controller: controller), child: buildChild(), ), ); }
看一下效果 :
紅包開啟其實就是將紅包上下兩部分分別進行向上和向下的平移,再加上背景顏色的漸變實現,示意圖如下:
加上之前金幣的動畫,存在多個動畫控制器,所以需要將 _RedPacketState 修改為混入 TickerProviderStateMixin:
class _RedPacketState extends State<RedPacket> with TickerProviderStateMixin{ /// ... }
然後在 RedPacketController 新增平移動畫控制器,並新增平移和顏色漸變動畫,平移係數從 0 到 1, 顏色漸變從不透明到完全透明。並將平移動畫與之前的金幣動畫合併為 repaint
。
class RedPacketController{ final TickerProviderStateMixin tickerProvider; late AnimationController angleController; late AnimationController translateController; late Animation<double> translateCtrl; late Animation<Color?> colorCtrl; void initAnimation() { /// ... translateController = AnimationController( duration: const Duration(milliseconds: 800), vsync: tickerProvider ); translateCtrl = translateController.drive(Tween(begin: 0.0, end: 1.0)); colorCtrl = translateController.drive(ColorTween( begin: Colors.redAccent, end: const Color(0x00FF5252)) ); repaint = Listenable.merge([angleController, translateController]); } }
再在 RedPacketPainter 的 super 裡傳入 repaint:
RedPacketPainter({required this.controller}) : super(repaint:controller.repaint);
改造繪製紅包上半部分程式碼:
void drawTop(ui.Canvas canvas) { canvas.save(); canvas.translate(0, topBezierEnd * ( - controller.translateCtrl.value)); /// ... canvas.restore(); }
新增畫布平移操作,平移的 Y 值為上半部分高度乘以動畫值,即從 0 向上移動上半部分高度。
下半部分新增同樣的處理,平移方向向下:
void drawBottom(ui.Canvas canvas) { canvas.save(); canvas.translate(0, topBezierStart * (controller.translateCtrl.value)); /// ... canvas.restore(); }
效果如下:
背景的平移效果實現了,但是上面的頭像和文字沒動,接下來給頭像和文字的 Widget 新增 AnimatedBuilder
使用相同的動畫讓其跟著移動:
Widget buildChild() { return AnimatedBuilder( animation: controller.translateController, builder: (context, child) => Container( padding: EdgeInsets.only(top: 0.3.sh * (1 - controller.translateCtrl.value)), child: Column(...), ), ); }
通過動態修改 paddingTop 的值,讓頭像與文字也向上平移。效果如下:
最後在金幣點選事件上新增一個定時器,金幣旋轉 2 秒後執行紅包開啟動畫:
void clickGold(TapUpDetails details) { if(checkClickGold(details.globalPosition)){ if(angleController.isAnimating){ stop(); }else{ angleController.repeat(reverse: true); tickerProvider.setState(() { showOpenText = false; }); timer = Timer(const Duration(seconds: 2), (){ stop(); }); } } } void stop() async{ if(angleController.isAnimating){ ///停止金幣動畫,讓動畫看起來更自然 if(angleController.status == AnimationStatus.forward){ await angleController.forward(); angleController.reverse(); }else if(angleController.status == AnimationStatus.reverse){ angleController.reverse(); } tickerProvider.setState(() { showOpenBtn = false; }); translateController.forward(); } }
這樣就實現了點選金幣後,金幣旋轉 2 秒後開啟紅包。
結果頁是一個新的介面,在紅包開啟時同步執行,並且擁有一個漸變動畫,路由跳轉時新增動畫實現,程式碼如下:
void onOpen(){ Navigator.push( context, PageRouteBuilder( transitionDuration: const Duration(seconds: 1), pageBuilder: (context, animation, secondaryAnimation) => FadeTransition( opacity: animation, child: const ResultPage(), ) ) ); }
給 RedPacket
新增一個紅包開啟時的回撥和紅包動畫完成時的回撥,前者用於跳轉結果頁,後者用於移除 Overlay
:
OverlayEntry? entry; void showRedPacket(BuildContext context, Function? onOpen){ entry = OverlayEntry(builder: (context) => RedPacket(onFinish: _removeRedPacket, onOpen: onOpen,)); Overlay.of(context)?.insert(entry!); } void _removeRedPacket(){ entry?.remove(); entry = null; }
在金幣旋轉動畫停止時呼叫:
void stop() async{ if(angleController.isAnimating){ /// ... translateController.forward(); onOpen?.call(); } }
紅包動畫完成時呼叫 onFinish 回撥, 給紅包最後的平移動畫新增監聽來實現:
translateController.addStatusListener((status) { if(status == AnimationStatus.completed){ onFinish?.call(); } });
OK,大功告成,再看看最終的效果:
以上就是Flutter利用Canvas模擬實現微信紅包領取效果的詳細內容,更多關於Flutter Canvas微信領紅包的資料請關注it145.com其它相關文章!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45