关于android:今日份分享Flutter自定义之旋转木马

先上图,带你回到童年时光:

成果剖析

  • 子布局依照圆形程序搁置且平分角度
  • 子布局旋转、反对手势滑动旋转、疾速滑动抬手持续旋转、主动旋转
  • 反对X轴旋转
  • 反对前后缩放子布局(起始角度为前,绝对地位为后,最后面最大,反而越小)
  • 多个布局叠加时后面遮挡前面

成果难点问题

  • 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

因为widthR都是已知并且定下来的尺寸,所以说,想要扭转中心点坐标,只需批改 角度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能够随便摆放一个组件,有点像相对布局。其中lefttop 、right、 bottom别离代表离Stack左、上、右、底四边的间隔。

Flutter之LayoutBuilder 组件

有时咱们心愿依据组件的大小确认组件的外观,比方竖屏的时候高低展现,横屏的时候左右展现,通过LayoutBuilder组件能够获取父组件的束缚尺寸。

附:github链接:https://github.com/yixiaolunh…

原文链接:https://www.jianshu.com/p/451…

文末

您的点赞珍藏就是对我最大的激励!
欢送关注我,分享Android干货,交换Android技术。
对文章有何见解,或者有何技术问题,欢送在评论区一起留言探讨!

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理