前期指路:

Flutter自绘组件:微信悬浮窗(一)

Flutter自绘组件:微信悬浮窗(二)

上两讲中讲解了微信悬浮窗按钮形态的实现,在本章中讲解如何实现悬浮窗列表形态。废话不多说,先上效果对比图。

效果对比

实现难点

这部分的难点主要有以下:

  1. 列表的每一项均是不规则的图形。

  2. 该项存在多个动画,如关闭时从屏幕中间返回至屏幕边缘的动画,关闭某项后该项往下的所有项向上平移的动画,以及出现时由屏幕边缘伸展至屏幕中间的动画。

  3. 列表中存在动画的衔接,如某列表项关闭是会有从中间返回至屏幕边缘的消失动画,且在消失之后,该列表项下面的列表项会产生一个往上移动的动画效果,如何做到这两个动画的无缝链接?

实现思路

列表项非规则图形,依旧按照按钮形态的方法,使用CustomPainterCustomPaint进行自定义图形的绘制。多个动画,根据触发的条件和环境不同,选择直接使用AnimationController进行管理或编写一个AnimatedWidget的子类,在父组件中进行管理。至于动画衔接部分,核心是状态管理。不同的列表项同属一个Widget,当其中一个列表项关闭完成后通知父组件列表,然后父组件再控制该列表项下的所有列表项进行一个自下而上的平移动画,直至到达关闭的列表项原位置。

这个组件的关键词列表动画,可能很多人已经想到了十分简单的实现方法,就是使用AnimatedList组件,它其内包含了增、删、插入时动画的接口,实现起来十分方便,但在本次中为了更深入了解状态管理和培养逻辑思维,并没有使用到这个组件,而是通过InheritedWidgetNotification的方法,完成了状态的传递,从而实现动画的衔接。在下一篇文章中会使用AnimatedList重写,读者可以把两种实现进行一个对比,加深理解。

使用到的新类

AnimationWidget:链接 :《Flutter实战》--动画结构

NotificationNotificationListener:链接:《Flutter实战》--Notification

InheritedWidget : 链接:《Flutter实战 》--数据共享

列表项图解及绘制代码

图解对比如下:

image

在设计的时候我把列表项的宽度设为屏幕的宽度的一般再加上50.0,左右列表项在中间的内容部分的布局是完全一样的,只是在外层部分有所不同,在绘制的时候,我分别把列表项的背景部分(背景阴影,外边缘,以及内层)、Logo部分、文字部分、交叉部分分别封装成了一个函数,避免了重复代码的编写,需要注意的是绘制Logo的Image对象的获取,在上一章中有讲到,此处不再详述。其他详情看代码及注释:

/// [FloatingItemPainter]:画笔类,绘制列表项class FloatingItemPainter extends CustomPainter{

FloatingItemPainter({@required this.title,@required this.isLeft,@required this.isPress,@required this.image});

/// [isLeft] 列表项在左侧/右侧  bool isLeft = true;/// [isPress] 列表项是否被选中,选中则绘制阴影  bool isPress;/// [title] 绘制列表项内容String title;/// [image] 列表项图标  ui.Image image;

@overridevoid paint(Canvas canvas, Size size) {// TODO: implement paintif(size.width < 50.0){return ;}else{if(isLeft){paintLeftItem(canvas, size);if(image != null)//防止传入null引起崩溃paintLogo(canvas, size);paintParagraph(canvas, size);paintCross(canvas, size);}else{paintRightItem(canvas, size);paintParagraph(canvas, size);paintCross(canvas, size);if(image != null)paintLogo(canvas, size);}}}

/// 通过传入[Canvas]对象和[Size]对象绘制左侧列表项外边缘,阴影以及内层void paintLeftItem(Canvas canvas,Size size){

/// 外边缘路径Path edgePath = new Path() ..moveTo(size.width - 25.0, 0.0);    edgePath.lineTo(0.0, 0.0);    edgePath.lineTo(0.0, size.height);    edgePath.lineTo(size.width - 25.0, size.height);    edgePath.arcTo(Rect.fromCircle(center: Offset(size.width - 25.0,size.height / 2),radius: 25), pi * 1.5, pi, true);

/// 绘制背景阴影    canvas.drawShadow(edgePath, Color.fromRGBO(0xDA, 0xDA, 0xDA, 0.3), 3, true);

var paint = new Paint()..style = PaintingStyle.fill..color = Colors.white;

/// 通过填充去除列表项内部多余的阴影    canvas.drawPath(edgePath, paint);

    paint = new Paint()..isAntiAlias = true  // 抗锯齿..style = PaintingStyle.stroke..color = Color.fromRGBO(0xCF, 0xCF, 0xCF, 1)..strokeWidth = 0.75..maskFilter = MaskFilter.blur(BlurStyle.solid, 0.25); //边缘模糊

/// 绘制列表项外边缘    canvas.drawPath(edgePath, paint);

/// [innerPath] 内层路径Path innerPath = new Path() ..moveTo(size.width - 25.0, 1.5);    innerPath.lineTo(0.0, 1.5);    innerPath.lineTo(0.0, size.height - 1.5);    innerPath.lineTo(size.width - 25.0, size.height - 1.5);    innerPath.arcTo(Rect.fromCircle(center: Offset(size.width - 25.0,size.height / 2),radius: 23.5), pi * 1.5, pi, true);

    paint = new Paint()..isAntiAlias = false..style = PaintingStyle.fill..color = Color.fromRGBO(0xF3, 0xF3, 0xF3, 1);

/// 绘制列表项内层    canvas.drawPath(innerPath, paint);

/// 绘制选中阴影if(isPress)      canvas.drawShadow(edgePath, Color.fromRGBO(0xDA, 0xDA, 0xDA, 0.3), 0, true);

}

/// 通过传入[Canvas]对象和[Size]对象绘制左侧列表项外边缘,阴影以及内层void paintRightItem(Canvas canvas,Size size){

/// 外边缘路径Path edgePath = new Path() ..moveTo(25.0, 0.0);    edgePath.lineTo(size.width, 0.0);    edgePath.lineTo(size.width, size.height);    edgePath.lineTo(25.0, size.height);    edgePath.arcTo(Rect.fromCircle(center: Offset(25.0,size.height / 2),radius: 25), pi * 0.5, pi, true);

/// 绘制列表项背景阴影    canvas.drawShadow(edgePath, Color.fromRGBO(0xDA, 0xDA, 0xDA, 0.3), 3, true);

var paint = new Paint()..style = PaintingStyle.fill..color = Colors.white;

/// 通过填充白色去除列表项内部多余阴影    canvas.drawPath(edgePath, paint);    paint = new Paint()..isAntiAlias = true..style = PaintingStyle.stroke..color = Color.fromRGBO(0xCF, 0xCF, 0xCF, 1)..strokeWidth = 0.75..maskFilter = MaskFilter.blur(BlurStyle.solid, 0.25); //边缘模糊

/// 绘制列表项外边缘    canvas.drawPath(edgePath, paint);

/// 列表项内层路径Path innerPath = new Path() ..moveTo(25.0, 1.5);    innerPath.lineTo(size.width, 1.5);    innerPath.lineTo(size.width, size.height - 1.5);    innerPath.lineTo(25.0, size.height - 1.5);    innerPath.arcTo(Rect.fromCircle(center: Offset(25.0,25.0),radius: 23.5), pi * 0.5, pi, true);

    paint = new Paint()..isAntiAlias = false..style = PaintingStyle.fill..color = Color.fromRGBO(0xF3, 0xF3, 0xF3, 1);

/// 绘制列表项内层    canvas.drawPath(innerPath, paint);

/// 条件绘制选中阴影if(isPress)      canvas.drawShadow(edgePath, Color.fromRGBO(0xDA, 0xDA, 0xDA, 0.3), 0, false);}

/// 通过传入[Canvas]对象和[Size]对象以及[image]绘制列表项Logovoid paintLogo(Canvas canvas,Size size){//绘制中间图标var paint = new Paint();    canvas.save(); //剪裁前保存图层RRect imageRRect = RRect.fromRectAndRadius(Rect.fromLTWH(25.0  - 17.5,25.0- 17.5, 35, 35),Radius.circular(17.5));    canvas.clipRRect(imageRRect);//图片为圆形,圆形剪裁    canvas.drawColor(Colors.white, BlendMode.srcOver); //设置填充颜色为白色Rect srcRect = Rect.fromLTWH(0.0, 0.0, image.width.toDouble(), image.height.toDouble());Rect dstRect = Rect.fromLTWH(25.0 - 17.5, 25.0 - 17.5, 35, 35);    canvas.drawImageRect(image, srcRect, dstRect, paint);    canvas.restore();//图片绘制完毕恢复图层}

/// 通过传入[Canvas]对象和[Size]对象以及[title]绘制列表项的文字说明部分void paintParagraph(Canvas canvas,Size size){

    ui.ParagraphBuilder pb  = ui.ParagraphBuilder(ui.ParagraphStyle(        textAlign: TextAlign.left,//左对齐        fontWeight: FontWeight.w500,        fontSize: 14.0, //字体大小        fontStyle: FontStyle.normal,        maxLines: 1, //行数限制        ellipsis: "…" //省略显示));

    pb.pushStyle(ui.TextStyle(color: Color.fromRGBO(61, 61, 61, 1),)); //字体颜色double pcLength = size.width - 100.0; //限制绘制字符串宽度    ui.ParagraphConstraints pc = ui.ParagraphConstraints(width: pcLength);    pb.addText(title);

    ui.Paragraph paragraph = pb.build() ..layout(pc);

Offset startOffset = Offset(50.0,18.0); // 字符串显示位置

/// 绘制字符串    canvas.drawParagraph(paragraph, startOffset);

}

/// 通过传入[Canvas]对象和[Size]对象绘制列表项末尾的交叉部分,void paintCross(Canvas canvas,Size size){

/// ‘x’ 路径Path crossPath = new Path()..moveTo(size.width - 28.5, 21.5);    crossPath.lineTo(size.width - 21.5,28.5);    crossPath.moveTo(size.width - 28.5, 28.5);    crossPath.lineTo(size.width - 21.5, 21.5);

var paint = new Paint()..isAntiAlias = true..color = Color.fromRGBO(61, 61, 61, 1)..style = PaintingStyle.stroke..strokeWidth = 0.75..maskFilter = MaskFilter.blur(BlurStyle.normal, 0.25); // 线段模糊

/// 绘制交叉路径    canvas.drawPath(crossPath, paint);}

@override  bool shouldRepaint(CustomPainter oldDelegate) {// TODO: implement shouldRepaintreturn (true && image != null);}}

列表项的实现代码

实现完列表项的绘制代码FloatingItemPainter类,你还需要一个画布CustomPaint和事件逻辑。一个完整列表项类除了绘制代码外还需要补充绘制区域的定位,列表项手势方法的捕捉(关闭和点击事件,关闭动画的逻辑处理。对于定位,纵坐标是根据传进来的top值决定的,对于列表项的Letf值则是根据列表项位于左侧 / 右侧的,左侧很好理解就为0。而右侧的坐标,由于列表项的长度为width + 50.0,因此列表项位于右侧时,横坐标为width - 50.0,如下图:

对于关闭动画,则是对横坐标Left取动画值来实现由中间收缩回边缘的动画效果。

对于事件的捕捉,需要确定当前列表项的点击区域和关闭区域。在事件处理的时候需要考虑较为极端的情况,就是把UI使用者不当正常人来看。正常的点击包括按下和抬起两个事件,但如果存在按下后拖拽出区域的情况呢?这时即使抬起后列表项还是处于选中的状态,还需要监听一个onTapCancel的事件,当拖拽离开列表项监听区域时将列表项设为未选中状态。

FloatingItem类的具体代码及解析如下:

/// [FloatingItem]一个单独功能完善的列表项类class FloatingItem extends StatefulWidget {

FloatingItem({@required this.top,@required this.isLeft,@required this.title,@required this.imageProvider,@required this.index,this.left,    Key key});/// [index] 列表项的索引值  int index;

/// [top]列表项的y坐标值  double top;/// [left]列表项的x坐标值  double left;

///[isLeft] 列表项是否在左侧,否则是右侧  bool isLeft;/// [title] 列表项的文字说明  String title;///[imageProvider] 列表项Logo的imageProvider  ImageProvider imageProvider;

@override  _FloatingItemState createState() => _FloatingItemState();

}

class _FloatingItemState extends State<FloatingItem> with TickerProviderStateMixin{

/// [isPress] 列表项是否被按下  bool isPress = false;

///[image] 列表项Logo的[ui.Image]对象,用于绘制Logo  ui.Image image;

/// [animationController] 列表关闭动画的控制器  AnimationController animationController;/// [animation] 列表项的关闭动画  Animation animation;/// [width] 屏幕宽度的一半,用于确定列表项的宽度  double width;

@overridevoid initState() {// TODO: implement initState    isPress = false;/// 获取Logo的ui.Image对象loadImageByProvider(widget.imageProvider).then((value) {setState(() {        image = value;});});super.initState();}

@override  Widget build(BuildContext context) {if(width == null)    width = MediaQuery.of(context).size.width / 2 ;if(widget.left == null)    widget.left = widget.isLeft ? 0.0 : width - 50.0;return Positioned(        left: widget.left,        top: widget.top,        child: GestureDetector(/// 监听按下事件,在点击区域内则将[isPress]设为true,若在关闭区域内则不做任何操作            onPanDown: (details) {if (widget.isLeft) {/// 点击区域内if (details.globalPosition.dx < width) {setState(() {                    isPress = true;});}}else{/// 点击区域内if(details.globalPosition.dx < width * 2 - 50){setState(() {                    isPress = true;});}}},/// 监听抬起事件            onTapUp: (details) async {/// 通过左右列表项来决定关闭的区域,以及选中区域,触发相应的关闭或选中事件if(widget.isLeft){/// 位于关闭区域if(details.globalPosition.dx >= width && !isPress){/// 设置从中间返回至边缘的关闭动画                  animationController = new AnimationController(vsync: this,duration:  new Duration(milliseconds: 100));                  animation = new Tween<double>(begin: 0.0,end: -(width + 50.0)).animate(animationController)..addListener(() {setState(() {                          widget.left = animation.value;});});/// 等待关闭动画结束后通知父级已关闭await animationController.forward();/// 销毁动画资源                  animationController.dispose();/// 通知父级触发关闭事件ClickNotification(deletedIndex: widget.index).dispatch(context);}else{/// 通知父级触发相应的点击事件ClickNotification(clickIndex: widget.index).dispatch(context);}}else{/// 位于关闭区域if(details.globalPosition.dx >= width * 2 - 50.0 && !isPress){/// 设置从中间返回至边缘的关闭动画                  animationController = new AnimationController(vsync: this,duration: new Duration(milliseconds: 100));                  animation = new Tween<double>(begin: width - 50.0,end: width * 2).animate(animationController)..addListener(() {setState(() {                        widget.left = animation.value;});});/// 等待执行完毕await animationController.forward();/// 销毁动画资源                  animationController.dispose();/// 通知父级触发关闭事件ClickNotification(deletedIndex: widget.index).dispatch(context);}else{/// 通知父级触发选中事件ClickNotification(clickIndex: widget.index).dispatch(context);}

}/// 抬起后取消选中setState(() {                isPress = false;});},            onTapCancel: (){/// 超出范围取消选中setState(() {                isPress = false;});},            child:CustomPaint(                size: new Size(width + 50.0,50.0),                painter: FloatingItemPainter(                  title: widget.title,                  isLeft: widget.isLeft,                  isPress: isPress,                  image: image,))));}

/// 通过ImageProvider获取ui.image  Future<ui.Image> loadImageByProvider(      ImageProvider provider, {        ImageConfiguration config = ImageConfiguration.empty,}) async {    Completer<ui.Image> completer = Completer<ui.Image>(); //完成的回调    ImageStreamListener listener;    ImageStream stream = provider.resolve(config); //获取图片流    listener = ImageStreamListener((ImageInfo frame, bool sync) {//监听final ui.Image image = frame.image;      completer.complete(image); //完成      stream.removeListener(listener); //移除监听});    stream.addListener(listener); //添加监听return completer.future; //返回}}

对于ClickNotification类,看一下代码:

import 'package:flutter/material.dart';

/// [ClickNotification]列表项点击事件通知类class ClickNotification extends Notification {ClickNotification({this.deletedIndex,this.clickIndex});/// 触发了关闭事件的列表项索引  int deletedIndex = -1;/// 触发了点击事件的列表项索引  int clickIndex = -1;}

它继承自Notification,自定义了一个通知用于处理列表项点击或关闭时整个列表发生的变化。单个列表项在执行完关闭动画后分发通知,通知父级进行一个列表项上移填补被删除列表项位置的的动画。

列表动画

单个列表项的关闭动画,我们已经在FlotingItem中实现了。而列表动画是,列表项关闭后,索引在其后的其他列表项向上平移填充的动画,示意图如下:

已知单个列表项的关闭动画是由自身管理实现的,那么单个列表项关闭后引起的列表动画由谁进行管理呢?自然是由列表进行管理。每个列表项除了原始的第一个列表项都可能会发生向上平移的动画,因此我们需要对单个的列表项再进行一层AnimatedWidget的加装,方便动画的传入与管理,具体代码如下:

FloatingItemAnimatedWidget:

/// [FloatingItemAnimatedWidget] 列表项进行动画类封装,方便传入平移向上动画class FloatingItemAnimatedWidget extends AnimatedWidget{

FloatingItemAnimatedWidget({    Key key,    Animation<double> animation,this.index,}):super(key:key,listenable: animation);

/// [index] 列表项索引final int index;

  @override  Widget build(BuildContext context) {// TODO: implement build/// 获取列表数据var data = FloatingWindowSharedDataWidget.of(context).data;final Animation<double> animation = listenable;return FloatingItem(top: animation.value, isLeft: data.isLeft, title: data.dataList[index]['title'],        imageProvider: AssetImage(data.dataList[index]['imageUrl']), index: index);}}

代码中引用到了一个新类FloatingWindowSharedDataWidget,它是一个InheritedWidget,共享了FloatingWindowModel类型的数据,FloatingWindowModel中包括了悬浮窗用到的一些数据,例如判断列表在左侧或右侧的isLeft,列表的数据dataList等,避免了父组件向子组件传数据时大量参数的编写,一定程度上增强了可维护性,例如FloatingItemAnimatedWidget中只需要传入索引值就可以在共享数据中提取到相应列表项的数据。FloatingWindowSharedDataWidgetFloatingWindowModel的代码及注释如下:

FloatingWindowSharedDataWidget

/// [FloatingWindowSharedDataWidget]悬浮窗数据共享Widgetclass FloatingWindowSharedDataWidget extends InheritedWidget{

FloatingWindowSharedDataWidget({@required this.data,  Widget child}) : super(child:child);

final FloatingWindowModel data;

/// 静态方法[of]方便直接调用获取共享数据  static FloatingWindowSharedDataWidget of(BuildContext context){return context.dependOnInheritedWidgetOfExactType<FloatingWindowSharedDataWidget>();}

  @override  bool updateShouldNotify(FloatingWindowSharedDataWidget oldWidget) {// TODO: implement updateShouldNotify/// 数据发生变化则发布通知return oldWidget.data != data && data.deleteIndex != -1;}}

FloatingWindowModel

/// [FloatingWindowModel] 表示悬浮窗共享的数据class FloatingWindowModel {

FloatingWindowModel({this.isLeft = true,this.top = 100.0,    List<Map<String,String>> datatList,}) : dataList = datatList;

/// [isLeft]:悬浮窗位于屏幕左侧/右侧  bool isLeft;

/// [top] 悬浮窗纵坐标  double top;

/// [dataList] 列表数据  List<Map<String,String>>dataList;

/// 删除的列表项索引  int deleteIndex = -1;}

列表的实现

上述已经实现了单个列表项并进行了动画的封装,现在只需要实现列表,监听列表项的点击和关闭事件并执行相应的操作。为了方便,我们实现了一个作为列表的FloatingItems类然后实现了一个悬浮窗类TestWindow来对列表的操作进行监听和管理,在以后的文章中还会继续完善TestWindow类和FloatingWindowModel类,把前两节的实现的FloatingButton加进去并实现联动。目前的具体实现代码和注释如下:

FloatingItems

/// [FloatingItems] 列表class FloatingItems extends StatefulWidget {@override  _FloatingItemsState createState() => _FloatingItemsState();}

class _FloatingItemsState extends State<FloatingItems> with TickerProviderStateMixin{

/// [_controller] 列表项动画的控制器  AnimationController _controller;

/// 动态生成列表/// 其中一项触发关闭事件后,索引在该项后的列表项执行向上平移的动画。  List<Widget> getItems(BuildContext context){/// 释放和申请新的动画资源if(_controller != null){      _controller.dispose();      _controller = new AnimationController(vsync: this,duration:  new Duration(milliseconds: 100));}/// widget列表    List<Widget>widgetList = [];/// 获取共享数据var data = FloatingWindowSharedDataWidget.of(context).data;/// 列表数据var dataList = data.dataList;/// 遍历数据生成列表项for(int i = 0; i < dataList.length; ++i){/// 在触发关闭事件列表项的索引之后的列表项传入向上平移动画if(data.deleteIndex != - 1 && i >= data.deleteIndex){        Animation animation;        animation = new Tween<double>(begin: data.top + (70.0 * (i + 1)),end: data.top + 70.0 * i).animate(_controller);        widgetList.add(FloatingItemAnimatedWidget(animation: animation,index: i));}/// 在触发关闭事件列表项的索引之前的列表项则位置固定else{        Animation animation;        animation = new Tween<double>(begin: data.top + (70.0 * i),end: data.top + 70.0 * i).animate(_controller);        widgetList.add(FloatingItemAnimatedWidget(animation: animation,index: i,));}}/// 执行动画if(_controller != null)      _controller.forward();/// 返回列表return widgetList;}

@overridevoid initState() {// TODO: implement initStatesuper.initState();    _controller = new AnimationController(vsync: this,duration: new Duration(milliseconds: 100));}

@override  Widget build(BuildContext context) {return Stack(children: getItems(context),);}}

TestWindow

/// [TestWindow] 悬浮窗class TestWindow extends StatefulWidget {@override  _TestWindowState createState() => _TestWindowState();}

class _TestWindowState extends State<TestWindow> {

  List<Map<String,String>> ls = [{'title': "测试以下","imageUrl":"assets/Images/vnote.png"},{'title': "Flutter自绘组件:微信悬浮窗(三)","imageUrl":"assets/Images/vnote.png"},{'title': "微信悬浮窗","imageUrl":"assets/Images/vnote.png"}];/// 悬浮窗数据类  FloatingWindowModel windowModel;

@overridevoid initState() {// TODO: implement initStatesuper.initState();    windowModel = new FloatingWindowModel(datatList: ls,isLeft: true);}

@override  Widget build(BuildContext context) {return FloatingWindowSharedDataWidget(      data: windowModel,      child:Stack(        fit: StackFit.expand, /// 未定义长宽的子类填充屏幕        children:[/// 遮盖层Container(                decoration:BoxDecoration(color: Color.fromRGBO(0xEF, 0xEF, 0xEF, 0.9))),/// 监听点击与关闭事件          NotificationListener<ClickNotification>(          onNotification: (notification) {/// 关闭事件if(notification.deletedIndex != - 1) {              windowModel.deleteIndex = notification.deletedIndex;setState(() {                windowModel.dataList.removeAt(windowModel.deleteIndex);});}if(notification.clickIndex != -1){/// 执行点击事件print(notification.clickIndex);}/// 禁止冒泡return false;},          child: FloatingItems(),),]));}}

main代码

void main(){runApp(MultiProvider(providers: [ChangeNotifierProvider<ClosingItemProvider>(      create: (_) => ClosingItemProvider(),)],    child: new MyApp(),),);}

class MyApp extends StatelessWidget {

@overrideWidget build(BuildContext context) {return MaterialApp(      title: 'Flutter Demo',      theme: new ThemeData(        primarySwatch: Colors.blue),      home: new Scaffold(        appBar: new AppBar(title: Text('Flutter Demo')),        body: Stack(          children: [/// 用于测试遮盖层是否生效Positioned(              left: 250,              top: 250,              child: Container(width: 50,height: 100,color: Colors.red,),),TestWindow()],)));}}

总结

对于列表项的编写,难度就在于状态的管理上和动画的管理上,绘制上来来去去还是那几个函数。组件存在多个复杂动画,每个动画由谁进行管理,如何触发,状态量如何传递,都是需要认真思考才能解决的提出的解决方案,本篇文章采用了一个比较“原始”的方式进行实现,但能使对状态的管理和动画的管理有更深入的理解,在下篇文章中采用更为简单的方式进行实现,通过AnimatedList即动画列表来实现。

flutter 图解_Flutter自绘组件:微信悬浮窗(三)相关推荐

  1. 前端悬浮窗效果_Flutter自绘组件:微信悬浮窗(一)

    看微信公众号的时候时常会想退出去回复消息,但又不想放弃已经阅读一半的文章,因为回复信息后再从公众号找到该篇文章之间有不必要的时间花费,微信悬浮窗的出现解决了这个烦恼,回复完消息之后只需要点击悬浮窗就可 ...

  2. flutter offset_Flutter自绘组件:微信悬浮窗(三)

    前期指路: Flutter自绘组件:微信悬浮窗(一) Flutter自绘组件:微信悬浮窗(二) 上两讲中讲解了微信悬浮窗按钮形态的实现,在本章中讲解如何实现悬浮窗列表形态.废话不多说,先上效果对比图. ...

  3. iOS电商常见动画与布局、微信悬浮窗、音乐播放器、歌词解析、拖动视图等源码

    iOS精选源码 MXScroll 介绍 混合使用UIScrollView ios 电商demo(实现各种常见动画效果和页面布局) 一行代码集成微信悬浮窗 可拖动,大小的视图,可放置在屏幕边缘. 在使用 ...

  4. autojs开启悬浮窗权限_微信悬浮窗功能普及?甚至更胜一筹

    最近微信的安卓版本更新了悬浮窗功能的确很好用!但悬浮窗有数量限制,而且仅限于微信.小狐日常在刷微博的时候,看到好的文章,当时又看不完,就在想,这时要是有微信的悬浮窗功能该多好! 甚至一些文章只看到一半 ...

  5. iOS高仿微信悬浮窗、忍者小猪游戏、音乐播放器、支付宝、今日头条布局滚动效果等源码...

    iOS精选源码 iOS WKWebView的使用源码 模仿apple music 小播放器的交互实现 高仿微信的悬浮小窗口 iOS仿支付宝首页效果 [swift]仿微信悬浮窗 类似于今日头条,网易新闻 ...

  6. android 仿ios悬浮窗,iOS仿微信悬浮窗

    仿微信悬浮窗,可直接协议加入悬浮窗或者直接调用方法注册,可自定义转场动画 演示 myFloat.gif 用法1 在Appdelegate中注册 传入对应控制器的className //只带控制器的cl ...

  7. web前端模仿微信悬浮窗效果

    微信新出了个悬浮窗的功能,因为业务需要,我用js写了个h5版本的,依赖jq或者zepto,可以自己选择改造. 请用手机或者电脑浏览器模拟手机模式查看 在线预览 代码如下 <!doctype ht ...

  8. html微信悬浮窗,微信新功能悬浮窗怎么用

    最先必须将微信升级到8.0.0之上版本号,才能够开展应用.最先进入微信,轻一点显示屏并拉下去,随后在未看了的文章内容下边,点一下文章内容进到,然后挑选右上方的三点,弹出对话框,点一下浮窗,待见到左上方 ...

  9. 开启微信悬浮窗权限有什么用_新版微信功能!微信也可以设置主题皮肤了,不再是单调的白色,这也太好看了吧!...

    阅读本文前,请您先点击上面的"蓝色字体",再点击"关注",这样您就可以继续免费收到文章了.每天都会有分享,都是免费订阅,请您放心关注.注:本文转载自网络,不代表 ...

最新文章

  1. [学习笔记]stm32
  2. 超详细的CMD文件讲解
  3. 密码机 密钥管理项目安装配置 从零开始
  4. linux的sonar安装,Linux安装sonar
  5. nginx配置二级域名
  6. 微软发布的新开源编程语言 Power Fx
  7. elcipse 编译cocos2d-x android
  8. 软件分层的利与不利之处.txt
  9. 计算机组成原理第五章考试题,计算机组成原理第五章部分课后题答案(唐朔飞版).doc...
  10. 《软件质量保证与测试》读书笔记(一)
  11. 短视频源码应该实现哪些功能;
  12. 思科路由器交换机指示灯状态详解
  13. app支付宝接入流程图_Android App支付系列(二):支付宝SDK接入详细指南(附官方支付demo)...
  14. 用了三年teambition的我,为什么改用飞项了?
  15. 教师资格证-教育知识与能力
  16. 无线鼠标 桌面服务器,你可能不知道 桌面总是乱糟糟的很可能是因为你没买对鼠标...
  17. uniapp onReachBottom 不触发
  18. matplotlib完美论文画图
  19. cocos2dx 网上资源
  20. IGBT/ MOSFET并联吸收电容:二阶电路零输入响应

热门文章

  1. redis做分布式锁可能不那么简单
  2. 常见面试题 - URL 解析
  3. python 基础笔记十一 - random模块
  4. POJ 1195 Mobile phones
  5. css 单行文本溢出显示省略号
  6. jni releative
  7. ssh客户端使用及下载
  8. C++中的static函数和extern关键字
  9. 如何在 InfoPath 2003 表单中动态加载数据
  10. TCP连接过程:三次握手与四次握手—Vecloud微云