Flutter自定义之旋转木马

简介: 本文详细介绍了如何在Flutter中实现子控件按照圆形顺序排列并平分角度,以及如何通过旋转、手势滑动和Stack组件实现3D效果,包括子控件的自动旋转、快速滑动手势响应和前后布局遮挡。

效果分析

  1. 子布局按照圆形顺序放置且平分角度
  2. 子布局旋转、支持手势滑动旋转、快速滑动抬手继续旋转、自动旋转
  3. 支持X轴旋转
  4. 支持前后缩放子布局(起始角度为前,相对位置为后,最前面最大,反而越小)
  5. 多个布局叠加时前面遮挡后面

效果难点问题

  • Flutter如何实现控件布局达到3D效果?
  • Flutter如何实现子控件旋转、自动旋转、手势滑动时关联子控件旋转滚动?快速滑动抬手继续旋转滚动?
  • Flutter如何实现多个布局叠加时前面遮挡后面?
1.子布局按照圆形顺序放置且平分角度

如上图所示:

如上图所示(参考系:最下方为0度,逆时针旋转角度增加)

第一个点
解:根据已知条件列方程式
x2=width/2+sin(a)*R
y2=height/2+cos(a)*R    

第二个点
解:根据已知条件列方程式①
① x=width/2-sin(b)*R 
   y=height/2-cos(b)*R
因为b=a-180,所以带入①方程得:
② x=width/2-sin(a-180)*R
   y=height/2-cos(a-180)*R 
又因为sin(k*360+a)=sin(a),所以②方式可以修改为:
③ x=width/2-sin(180+a)*R
   y=height/2-cos(180+a)*R
又又因为 sin(180+a)=-sin(a),cos(180+a)=-cosa 带入③方程式得:
④ x=width/2+sin(a)*R 
  y=height/2+cos(a)*R 
  
由上面2点计算得,每个子布局的中心点坐标公式统一为:
x=width/2+sin(a)*R 
y=height/2+cos(a)*R 

以上所用三角函数公式表:

通过上面计算得出子控件的位置公式后,开始我们的代码。

实现子控件按照圆形布局及平分角度代码如下:

//所有子控件的位置数据
//count:子控件数量;  
//startAngle:开始角度默认为0;  
//rotateAngle:偏转角度默认为0;
List<Point> _childPointList({Size size = Size.zero}) {
    List<Point> childPointList = [];
    double averageAngle = 360 / count;
    double radius = size.width / 2 - childWidth / 2;   
    for (int i = 0; i < count; i++) {
       /********************子布局角度*****************/
      double angle = startAngle + averageAngle * i + rotateAngle;
      //子布局中心点坐标
      var centerX = size.width / 2 + sin(radian(angle)) * radius;
      var centerY = size.height / 2 + cos(radian(angle)) * radius;
      childPointList.add(Point(
        centerX,
        centerY,
        childWidth,
        childHeight,
        centerX - childWidth / 2,
        centerY - childHeight / 2,
        centerX + childWidth / 2,
        centerY + childHeight / 2,
        1,
        angle,
        i,
      ));
    }
    return childPointList;
  }

///角度转弧度
///弧度 =度数 * (π / 180)
///度数 =弧度 * (180 / π)
double radian(double angle) {
    return angle * pi / 180;
}
2.子布局如何旋转?自动旋转?支持手势滑动旋转?快速滑动抬手继续旋转?
子布局如何旋转

所谓的旋转就是所有的子布局绕着圆形移动,布局一旦移动就代表中间位置改变,根据上面我们计算的子布局位置的公式来看:

中心点坐标
x=width/2+sin(a)*R 
y=height/2+cos(a)*R 

因为width和R都是已知并且定下来的尺寸,所以说,想要改变中心点坐标,只需修改 角度a就可以了。要想达到旋转效果的话就是让所有的子布局都同时移动相同的角度即可。

子布局原始角度值:
double angle = startAngle + averageAngle * i; 
我们可以在此基础上加上一个可变的角度值,通过改变这个值,所有的子布局都会同时加上此值同时移动了位置。如下:
double angle = startAngle + averageAngle * i + rotateAngle; 
其中 rotateAngle 就是可变的值。改变这个值就能让布局动起来
自动旋转

同理,我们只要搞个定时器,周期性修改这个rotateAngle值,并setState(() {})刷新下,看起来就自动旋转了。

支持手势滑动旋转

大家已经知道通过修改rotateAngle值去实现旋转,那么支持手势滑动旋转顾名思义就是通过手势修改这个rotateAngle值就OK,那么手势处理Flutter提供了GestureDetector组件,这个组件功能很强大,这里面我们使用了他的几个回调方法。

本次实现直接使用水平滑动监听,大家如果想兼容竖直滑动可以自己尝试修改就可以。
GestureDetector(
        ///水平滑动按下
        onHorizontalDragDown: (DragDownDetails details) {...},
        
        ///水平滑动开始
        onHorizontalDragStart: (DragStartDetails details) {
          //记录拖动开始时当前的选择角度值
          downAngle = rotateAngle;
          //记录拖动开始时的x坐标
          downX = details.globalPosition.dx;
        },

        ///水平滑动中
        onHorizontalDragUpdate: (DragUpdateDetails details) {
           //滑动中X坐标值
          var updateX = details.globalPosition.dx;
          //计算当前旋转角度值并刷新
          rotateAngle = (downX - updateX) * slipRatio + downAngle;
          if (mounted) setState(() {});
        },

        ///水平滑动结束
        onHorizontalDragEnd: (DragEndDetails details) {...},
        
        ///滑动取消
        onHorizontalDragCancel: () {...},
        
        behavior: HitTestBehavior.opaque,//deferToChild   translucent
        child: xxx,
);
快速滑动抬手继续旋转

抬手还能继续旋转,也就是当我们快速滑动抬手的时候只要继续修改旋转角度值rotateAngle就可以达到继续旋转的效果。当我们抬手的时候我们可以拿到什么呢?

例如:当我们骑着小黄单车在大路上快速的蹬着脚蹬子然后停止蹬,你的小黄已当时的速度飞驰在这个大路上,由于地面的摩擦力的影响,速度会越来越小,最后停止。

 ///水平滑动结束
onHorizontalDragEnd: (DragEndDetails details) {
          //x方向上每秒速度的像素数
          velocityX = details.velocity.pixelsPerSecond.dx; 
          _controller.reset();
          _controller.forward();
 },
 
 
  //动画设置rotateAngle
   _controller = AnimationController(
      vsync: this,
      duration: Duration(milliseconds: 1000),
    );

    animation = CurvedAnimation(
      parent: _controller,
      curve: Curves.linearToEaseOut,
    );

    animation = new Tween<double>(begin: 1, end: 0).animate(animation)
      ..addListener(() {
        //当前速度
        var velocity = animation.value * -velocityX;
        var offsetX = radius != 0 ? velocity * 5 / (2 * pi * radius) : velocity;
        rotateAngle += offsetX;
        setState(() => {});
      })
      ..addStatusListener((status) {
        if (status == AnimationStatus.completed) {
          rotateAngle = rotateAngle % 360;
          _startRotateTimer();
        }
      });

####3.支持X轴旋转

上图是X轴方向查看旋转切面图,按照x轴旋转所有的x坐标都是相同的,y值从上往下不断增加。因为绕着X轴旋转时,X坐标是不变的,Y坐标值改变,当旋转了a角度时,现在的Y坐标如图所示为

Y坐标旋转后=height/2+y*cos(a)     y值我们已经在上面计算过了,y=cos(a)*R 
所以最终的计算公式是:
Y坐标值=height/2+cos(a)*R*cos(a)
cos(a)在a=[0,90]区间时对应的值是1-0   即是 a=0度时cos(a)=1,就是原始状态(与Y轴平行),a=90度时cos(a)=0,就是与Y轴垂直准状态。所以我们可以设置a在0-90之间即可。

####4.支持前后缩放子布局(起始角度为前,相对位置为后,最前面最大,反而越小)

上图为cos余弦曲线图。0度和360度最大 ,180度最小,刚好与我们设计的初始值从0开始,然后逆时针绕一圈角度从0-360度。

所以缩放比scale计算公式可以写为:

 var scale = (1 - minScale) / 2 * (1 + cos(radian(angle - startAngle))) + minScale;
 minScale为最小缩放比,为了让缩放有个极限值,即 scale范围为:(minScale,1)
5.多个布局叠加时前面遮挡后面

从视觉感受,靠近前面的布局应该遮挡后面的布局,在Android当中bringToFront()方法可以让布局置于前面,Flutter没有提供此方法,我们该如何处理这种情况呢?

Flutter提供一个Stack布局,也叫层叠式布局,当我们添加子布局到Stack布局中时,后面添加的会遮住前面添加的,所以只要我们在添加子布局的时候按照由后到前来添加即可。话说怎么知道是前是后呢?


知道实现思路现在要解决的问题是:


如何区分前与后?有什么条件可以区分?


###考虑中…


####1、根据坐标值? Y坐标小就是后面,Y坐标大就是前面?


答案是不一定;因为当我启动角度不是0的时候,比如是90度,那么最右面是前面,最左边是后面,这个时候是X坐标的大小区分前后关系,所以说单独使用坐标值的大小来决定前后关系是不对的。


####2、根据前大后小原则?根据缩放值排序来添加子布局?


答案是可行;因为我们已经实现了前面的布局缩放值是1,后面的缩放值越来越小,而且我们已经处理了启动角度问题,所以根据缩放值来实现是可行的。

///通过缩放值进行排序,从小到大
childPointList.sort((a, b) {
  return a.scale.compareTo(b.scale);
});

///遍历添加子布局
Stack(
  children: childPointList.map(
              (Point point) {
                return Positioned(
                    width: point.width,
                    left: point.left,
                    top: point.top,
                    child: this.widget.children[point.index]);
              },
            ).toList(),
   ),

通过上面方式即可实现前后遮挡效果了。

小知识点

Flutter 之Stack 组件

Stack一个可以叠加子控件的布局,这里主要讲一下 Positioned,其他使用方式可以看下官网说明。

Positioned({
  Key key,
  this.left,
  this.top,
  this.right,
  this.bottom,
  this.width,
  this.height,
  @required Widget child,
})

使用Positioned控制Widget的位置,通过Positioned可以随意摆放一个组件,有点像绝对布局。其中left、top 、right、 bottom分别代表离Stack左、上、右、底四边的距离。

Flutter之LayoutBuilder 组件

有时我们希望根据组件的大小确认组件的外观,比如竖屏的时候上下展示,横屏的时候左右展示,通过LayoutBuilder组件可以获取父组件的约束尺寸。

附:github链接:https://github.com/yixiaolunhui/my_flutter

相关文章
|
1月前
|
UED 开发者 容器
Flutter&鸿蒙next 的 Sliver 实现自定义滚动效果
Flutter 提供了强大的滚动组件,如 ListView 和 GridView,但当需要更复杂的滚动效果时,Sliver 组件是一个强大的工具。本文介绍了如何使用 Sliver 实现自定义滚动效果,包括 SliverAppBar、SliverList 等常用组件的使用方法,以及通过 CustomScrollView 组合多个 Sliver 组件实现复杂布局的示例。通过具体代码示例,展示了如何实现带有可伸缩 AppBar 和可滚动列表的页面。
110 1
|
1月前
Flutter 自定义组件继承与调用的高级使用方式
本文深入探讨了 Flutter 中自定义组件的高级使用方式,包括创建基本自定义组件、继承现有组件、使用 Mixins 和组合模式等。通过这些方法,您可以构建灵活、可重用且易于维护的 UI 组件,从而提升开发效率和代码质量。
131 1
|
1月前
|
前端开发 开发者
深入探索 Flutter 鸿蒙版的画笔使用与高级自定义动画
本文深入探讨了 Flutter 中的绘图功能,重点介绍了 CustomPainter 和 Canvas 的使用方法。通过示例代码,详细讲解了如何绘制自定义图形、设置 Paint 对象的属性以及实现高级自定义动画。内容涵盖基本绘图、动画基础、渐变动画和路径动画,帮助读者掌握 Flutter 绘图与动画的核心技巧。
80 1
|
1月前
|
Dart UED 开发者
Flutter&鸿蒙next中的按钮封装:自定义样式与交互
在Flutter应用开发中,按钮是用户界面的重要组成部分。Flutter提供了多种内置按钮组件,但有时这些样式无法满足特定设计需求。因此,封装一个自定义按钮组件变得尤为重要。自定义按钮组件可以确保应用中所有按钮的一致性、可维护性和可扩展性,同时提供更高的灵活性,支持自定义颜色、形状和点击事件。本文介绍了如何创建一个名为CustomButton的自定义按钮组件,并详细说明了其样式、形状、颜色和点击事件的处理方法。
86 1
|
1月前
|
Dart 搜索推荐 API
Flutter & 鸿蒙next版本:自定义对话框与表单验证的动态反馈与错误处理
在现代移动应用开发中,用户体验至关重要。本文探讨了如何在 Flutter 与鸿蒙操作系统(HarmonyOS)中创建自定义对话框,并结合表单验证实现动态反馈与错误处理,提升用户体验。通过自定义对话框和表单验证,开发者可以提供更加丰富和友好的交互体验,同时利用鸿蒙next版本拓展应用的受众范围。
84 1
|
3月前
|
前端开发 搜索推荐
Flutter中自定义气泡框效果的实现
Flutter中自定义气泡框效果的实现
114 3
|
4月前
|
前端开发
Flutter快速实现自定义折线图,支持数据改变过渡动画
Flutter快速实现自定义折线图,支持数据改变过渡动画
109 4
Flutter快速实现自定义折线图,支持数据改变过渡动画
|
4月前
|
开发者 监控 开发工具
如何将JSF应用送上云端?揭秘在Google Cloud Platform上部署JSF应用的神秘步骤
【8月更文挑战第31天】本文详细介绍如何在Google Cloud Platform (GCP) 上部署JavaServer Faces (JSF) 应用。首先,确保已准备好JSF应用并通过Maven构建WAR包。接着,使用Google Cloud SDK登录并配置GCP环境。然后,创建`app.yaml`文件以配置Google App Engine,并使用`gcloud app deploy`命令完成部署。最后,通过`gcloud app browse`访问应用,并利用GCP的监控和日志服务进行管理和故障排查。整个过程简单高效,帮助开发者轻松部署和管理JSF应用。
64 0
|
4月前
|
开发者 容器 Java
Azure云之旅:JSF应用的神秘部署指南,揭开云原生的新篇章!
【8月更文挑战第31天】本文探讨了如何在Azure上部署JavaServer Faces (JSF) 应用,充分发挥其界面构建能力和云平台优势,实现高效安全的Web应用。Azure提供的多种服务如App Service、Kubernetes Service (AKS) 和DevOps简化了部署流程,并支持应用全生命周期管理。文章详细介绍了使用Azure Spring Cloud和App Service部署JSF应用的具体步骤,帮助开发者更好地利用Azure的强大功能。无论是在微服务架构下还是传统环境中,Azure都能为JSF应用提供全面支持,助力开发者拓展技术视野与实践机会。
21 0
|
4月前
|
开发框架 API 开发者
Flutter表单控件深度解析:从基本构建到高级自定义,全方位打造既美观又实用的移动端数据输入体验,让应用交互更上一层楼
【8月更文挑战第31天】在构建美观且功能强大的移动应用时,表单是不可或缺的部分。Flutter 作为热门的跨平台开发框架,提供了丰富的表单控件和 API,使开发者能轻松创建高质量表单。本文通过问题解答形式,深入解读 Flutter 表单控件,并通过具体示例代码展示如何构建优秀的移动应用表单。涵盖创建基本表单、处理表单提交、自定义控件样式、焦点管理和异步验证等内容,适合各水平开发者学习和参考。
111 0