首页 > 其他分享 >Flutter仿抖音项目(改)

Flutter仿抖音项目(改)

时间:2022-10-09 02:11:05浏览次数:53  
标签:widget return 项目 value dart 仿抖音 key child Flutter

Flutter 仿抖音项目(改)

项目地址 mjl0602/flutter_tiktok: Flutter tiktok short video app. (github.com)

SSH : [email protected]:mjl0602/flutter_tiktok.git

应用截图

image

应用功能

  • 上下刷视频,同时自动加载封面
  • 实现刷新页面
  • 左滑右滑实现个人中心
  • 双击出现爱心和点赞
  • 看评论
  • 看关注
  • 看收藏
  • 底部tabbar的切换

细节

  • 适应了不同屏幕比例
    image

项目结构

  • 依赖:
 # 加载动画库(好像改版之后就没用到了)
flutter_spinkit: ^4.1.2
  # Bilibili开源的视频播放组件
fijkplayer: ^0.8.3
  # 基础的透明动画点击效果
tapped: any
  # map安全取值
safemap: any
  • 主要文件
./lib
├── main.dart
├── mock
│   └── video.dart # 假数据
├── other
│   └── bottomSheet.dart # 修改了系统BottomSheet的高度
├── pages
│   ├── cameraPage.dart # 拍摄页(没有实际功能)
│   ├── followPage.dart  # 略
│   ├── homePage.dart # 主页面,包含tikTokScaffold的实际应用功能
│   ├── msgDetailListPage.dart # 略
│   ├── msgPage.dart # 略
│   ├── searchPage.dart # 略
│   ├── todoPage.dart # 略
│   ├── userDetailPage.dart # 略
│   ├── userPage.dart # 略
│   └── walletPage.d # 略
├── style
│   ├── style.dart # 全局文字大小与颜色
│   └── text.dart # 主要的几个文字样式
└── views
    ├── backButton.dart # iOS形状的返回按钮组件
    ├── loadingButton.dart # 可以设置为载入样式的按钮组件
    ├── selectText.dart # 可设置为“选中”或者“未选中”样式的文字
    ├── tikTokCommentBottomSheet.dart # 仿Tiktok评论样式
    ├── tikTokHeader.dart # 仿Tiktok顶部切换组件
    ├── tikTokScaffold.dart # 仿Tiktok核心脚手架,封装了手势与切换等功能,本身不包含UI内容
    ├── tikTokVideo.dart # 仿Tiktok的视频UI样式封装,不包含视频播放
    ├── tikTokVideoButtonColumn.dart # 仿Tiktok视频右侧的头像与点赞等按钮列的组件
    ├── tikTokVideoGesture.dart # 仿Tiktok的双击点赞效果
    ├── tikTokVideoPlayer.dart # 视频播放页面,带有控制滑动的VideoListController类
    ├── tiktokTabBar.dart # 仿Tiktok的底部Tabbar组件
    ├── tilTokAppBar.dart # 仿Tiktok的Appbar组件
    ├── topToolRow.dart # 用户页面的顶部状态,在tab切换到user页面时隐藏返回按钮
    └── userMsgRow.dart # 一条用户信息的样式组件

我做出的修改

修改前示意

修改后示意

修改代码路径及内容

lib/views/TikTokButtonColumn.dart
修改内容:修改了点击红心的颜色(右侧的红心,评论,分享侧边栏)

lib/views/TikTokVideoGesture.dart

  1. 经过测试,原本项目利用点击屏幕的坐标设置为GeatureDector的Key,如果反复点击屏幕的同一位置,会出现Key重复导致Bug,优化了代码结构之后,去除掉了Offset的转化函数,并且使用context.findRenderObject() as RenderBox 转化,
  2. 优化了项目自带的动画效果,本来的Transform.rotate套一个Transform.scale,实现爱心的旋转和先变小后变大的效果。现在的修改版本,通过爆改函数,实现了类似抖音的动画平移,爱心旋转,爱心缩小后扩大的效果,同时优化了颜色等细节,目前的点赞十分类似抖音原版
    (因为个人抖音账号原因,在这里就不放入抖音点赞效果了,可以打开原版抖音看一下)

修改部分示意

  • 原本代码(TikTokVideoGesture.dart)
import 'dart:async';
import 'dart:math';

import 'package:flutter/material.dart';

/// 视频手势封装
/// 单击:暂停
/// 双击:点赞,双击后再次单击也是增加点赞爱心
class TikTokVideoGesture extends StatefulWidget {
  const TikTokVideoGesture({
    Key? key,
    required this.child,
    this.onAddFavorite,
    this.onSingleTap,
  }) : super(key: key);

  final Function? onAddFavorite;
  final Function? onSingleTap;
  final Widget child;

  @override
  _TikTokVideoGestureState createState() => _TikTokVideoGestureState();
}

class _TikTokVideoGestureState extends State<TikTokVideoGesture> {
  GlobalKey _key = GlobalKey();

  // 内部转换坐标点
  Offset _p(Offset p) {
    RenderBox getBox = _key.currentContext!.findRenderObject() as RenderBox;
    return getBox.globalToLocal(p);
  }

  List<Offset> icons = [];

  bool canAddFavorite = false;
  bool justAddFavorite = false;
  Timer? timer;

  @override
  Widget build(BuildContext context) {
    var iconStack = Stack(
      children: icons
          .map<Widget>(
            (p) => TikTokFavoriteAnimationIcon(
              key: Key(p.toString()),
              position: p,
              onAnimationComplete: () {
                icons.remove(p);
              },
            ),
          )
          .toList(),
    );
    return GestureDetector(
      key: _key,
      onTapDown: (detail) {
        setState(() {
          if (canAddFavorite) {
            print('添加爱心,当前爱心数量:${icons.length}');
            icons.add(_p(detail.globalPosition));
            widget.onAddFavorite?.call();
            justAddFavorite = true;
          } else {
            justAddFavorite = false;
          }
        });
      },
      onTapUp: (detail) {
        timer?.cancel();
        var delay = canAddFavorite ? 1200 : 600;
        timer = Timer(Duration(milliseconds: delay), () {
          canAddFavorite = false;
          timer = null;
          if (!justAddFavorite) {
            widget.onSingleTap?.call();
          }
        });
        canAddFavorite = true;
      },
      onTapCancel: () {
        print('onTapCancel');
      },
      child: Stack(
        children: <Widget>[
          widget.child,
          iconStack,
        ],
      ),
    );
  }
}

class TikTokFavoriteAnimationIcon extends StatefulWidget {
  final Offset? position;
  final double size;
  final Function? onAnimationComplete;

  const TikTokFavoriteAnimationIcon({
    Key? key,
    this.onAnimationComplete,
    this.position,
    this.size: 100,
  }) : super(key: key);

  @override
  _TikTokFavoriteAnimationIconState createState() =>
      _TikTokFavoriteAnimationIconState();
}

class _TikTokFavoriteAnimationIconState
    extends State<TikTokFavoriteAnimationIcon> with TickerProviderStateMixin {
  AnimationController? _animationController;
  @override
  void dispose() {
    _animationController?.dispose();
    super.dispose();
  }

  @override
  void didChangeDependencies() {
    print('didChangeDependencies');
    super.didChangeDependencies();
  }

  @override
  void initState() {
    _animationController = AnimationController(
      lowerBound: 0,
      upperBound: 1,
      duration: Duration(milliseconds: 1600),
      vsync: this,
    );

    _animationController!.addListener(() {
      setState(() {});
    });
    startAnimation();
    super.initState();
  }

  startAnimation() async {
    await _animationController!.forward();
    widget.onAnimationComplete?.call();
  }

  double rotate = pi / 10.0 * (2 * Random().nextDouble() - 1);

  double? get value => _animationController?.value;

  double appearDuration = 0.1;
  double dismissDuration = 0.8;

  double get opa {
    if (value! < appearDuration) {
      return 0.99 / appearDuration * value!;
    }
    if (value! < dismissDuration) {
      return 0.99;
    }
    var res = 0.99 - (value! - dismissDuration) / (1 - dismissDuration);
    return res < 0 ? 0 : res;
  }

  double get scale {
    if (value! < appearDuration) {
      return 1 + appearDuration - value!;
    }
    if (value! < dismissDuration) {
      return 1;
    }
    return (value! - dismissDuration) / (1 - dismissDuration) + 1;
  }

  @override
  Widget build(BuildContext context) {
    Widget content = Icon(
      Icons.favorite,
      size: widget.size,
      color: Colors.redAccent,
    );
    content = ShaderMask(
      child: content,
      blendMode: BlendMode.srcATop,
      shaderCallback: (Rect bounds) => RadialGradient(
        center: Alignment.topLeft.add(Alignment(0.66, 0.66)),
        colors: [
          Color(0xffEF6F6F),
          Color(0xffF03E3E),
        ],
      ).createShader(bounds),
    );
    Widget body = Transform.rotate(
      angle: rotate,
      child: Opacity(
        opacity: opa,
        child: Transform.scale(
          alignment: Alignment.bottomCenter,
          scale: scale,
          child: content,
        ),
      ),
    );
    return widget.position == null
        ? Container()
        : Positioned(
            left: widget.position!.dx - widget.size / 2,
            top: widget.position!.dy - widget.size / 2,
            child: body,
          );
  }
}

  • 修改后代码
import 'dart:async';
import 'dart:math';

import 'package:flutter/material.dart';

/// 视频手势封装
/// 单击:暂停
/// 双击:点赞,双击后再次单击也是增加点赞爱心
class TikTokVideoGesture extends StatefulWidget {
  const TikTokVideoGesture({
    Key? key,
    required this.child,
    this.onAddFavorite,
    this.onSingleTap,
  }) : super(key: key);

  final Function? onAddFavorite;
  final Function? onSingleTap;
  final Widget child;

  @override
  _TikTokVideoGestureState createState() => _TikTokVideoGestureState();
}

class _TikTokVideoGestureState extends State<TikTokVideoGesture> {
  // 内部转换坐标点

  List<Offset> icons = [];

  bool canAddFavorite = false;
  bool justAddFavorite = false;
  Timer? timer;

  @override

  Widget build(BuildContext context) {
    var iconStack = Stack(
      children: icons
          .map<Widget>(
            (p) => TikTokFavoriteAnimationIcon(
              key: Key(p.toString()),
              position: p,
              onAnimationComplete: () {
                icons.remove(p);
              },
            ),
          )
          .toList(),
    );
    try {
      return GestureDetector(
        onTapDown: (detail) {
          var box = context.findRenderObject() as RenderBox;
          var pos = box.globalToLocal(detail.globalPosition);
          setState(() {
            if (canAddFavorite) {
              print('添加爱心,当前爱心数量:${icons.length}');
              icons.add(pos);
              widget.onAddFavorite?.call();
              justAddFavorite = true;
            } else {
              justAddFavorite = false;
            }
          });
        },
        onTapUp: (detail) {
          timer?.cancel();
          var delay = canAddFavorite ? 1200 : 600;
          timer = Timer(Duration(milliseconds: delay), () {
            canAddFavorite = false;
            timer = null;
            if (!justAddFavorite) {
              widget.onSingleTap?.call();
            }
          });
          canAddFavorite = true;
        },
        onTapCancel: () {
          print('onTapCancel');
        },
        child: Stack(
          children: <Widget>[
            widget.child,
            iconStack,
          ],
        ),
      );
    } catch (e) {
      print(e);
      return Container();
    }
  }
}

class TikTokFavoriteAnimationIcon extends StatefulWidget {
  final Offset? position;
  final double size;
  final Function? onAnimationComplete;

  const TikTokFavoriteAnimationIcon({
    Key? key,
    this.onAnimationComplete,
    this.position,
    this.size: 100,
  }) : super(key: key);

  @override
  _TikTokFavoriteAnimationIconState createState() =>
      _TikTokFavoriteAnimationIconState();
}

class _TikTokFavoriteAnimationIconState
    extends State<TikTokFavoriteAnimationIcon> with TickerProviderStateMixin {
  AnimationController? _animationController;
  @override
  void dispose() {
    _animationController?.dispose();
    super.dispose();
  }

  @override
  void didChangeDependencies() {
    print('didChangeDependencies');
    super.didChangeDependencies();
  }

  @override
  void initState() {
    _animationController = AnimationController(
      lowerBound: 0.75,
      upperBound: 1,
      duration: Duration(milliseconds: 1500),
      vsync: this,
    );

    _animationController!.addListener(() {
      setState(() {});
    });
    startAnimation();
    super.initState();
  }

  startAnimation() async {
    await _animationController!.forward();
    widget.onAnimationComplete?.call();
  }

  double rotate = pi / 10.0 * (2 * Random().nextDouble() - 1);

  double? get value => _animationController?.value;

  double appearDuration = 0.8;
  double dismissDuration = 0.95;

  double get opa {
    if (value! < appearDuration) {
      return 0.99 / appearDuration * value!;
    }
    if (value! < dismissDuration) {
      return 0.99;
    }
    var res = 0.99 - (value! - dismissDuration) / (1 - dismissDuration);
    return res < 0 ? 0 : res;
  }

  double get scale {
    const k = 400;
    if (value! < appearDuration) {
      var temp1 = k * (value! - 0.775) * (value! - 0.8) + 1;
      print(temp1);
      return temp1;
    }
    if (value! < dismissDuration) {
      return 1;
    }
    const axis = 1.05;
    var k1 = axis - dismissDuration;
    return -(k1) / (value! - axis);
  }

  double get top {
    if (value! < dismissDuration)
      return (sqrt(dismissDuration)) * 150 +
          (pow(dismissDuration, 3)) * 100 -
          50;
    return (sqrt(value!)) * 150 + (value! * value! * value!) * 100 - 50;
  }

  @override
  Widget build(BuildContext context) {
    Widget content = Icon(
      Icons.favorite,
      size: widget.size,
      color: Colors.redAccent,
    );
    content = ShaderMask(
      child: content,
      blendMode: BlendMode.srcATop,
      shaderCallback: (Rect bounds) => RadialGradient(
        center: Alignment.topLeft.add(Alignment(0.66, 0.66)),
        colors: [
          Colors.pink.shade200,
          Color(0xffE91E63),
        ],
      ).createShader(bounds),
    );
    Widget body = Transform.rotate(
      angle: rotate,
      child: Opacity(
        opacity: opa,
        child: Transform.scale(
          alignment: Alignment.bottomCenter,
          scale: scale,
          child: content,
        ),
      ),
    );
    return widget.position == null
        ? Container()
        : Positioned(
            left: widget.position!.dx - widget.size / 2,
            //top: widget.position!.dy - widget.size / 2,
            top: widget.position!.dy - widget.size / 2 + 50 - top,
            child: body,
          );
  }
}

小结

  • 代码和commit在我自己电脑里,到时候可以直接展示
  • 了解了一点RanderBox的原理
  • 学到了一些除去Staggered Animations的组合动画实现方式

标签:widget,return,项目,value,dart,仿抖音,key,child,Flutter
From: https://www.cnblogs.com/ZzTzZ/p/16770821.html

相关文章