Flutter 什么,微信现在才支持实况图!?

相关阅读

是不是你们最喜欢看的?这是一段有声音的 Gif ,流口水声。

1726883654762.gif

前言

最近微信朋友圈可以发实况图了,上了热搜!

305c789787bb14208d6b402ddf4d0e89.png

我在想,好家伙, 实况图 (Live Photo) iOS 9.1 就支持了,现在都 iOS 18 了。

0f615f71644dd9e3f46f8e28229dc91b.jpg

我记得 photo_managerwechat_assets_picker 很早就支持了呀!

image.png

image.png

虽然但是,然后需求它又来了。

IMG_20240920_161542.jpg

IMG_20240920_161352.jpg

微信都不支持,不要太卷了,年轻人。

什么是 实况图 (Live Photo)

要编写这种效果,首先我们要了解一下 实况图 (Live Photo) 是什么。

首先在手机上面制作一个实况图 (Live Photo), 通过隔空投送,发现只有一张 HEIC 格式的图片。

live图片隔空投送到mac上变成HEIC模式… – Apple 社区

后来找到另外的方式,就是在 mac 上面登录跟你手机相同的账号,从相册应用中找到该图片,从 File-> Export-> Export Unmodified Original For 1 Photo文件-> 导出-> 导出未处理的原片

objective c – Can I put a live photo into the iOS Simulator? – Stack Overflow

DM_20240921101555_001.png

导出之后,是 2 个文件,一个是图片,一个是视频。

image.png

当然,你也可以直接使用 photo_manager 读取你手机中的 实况图 (Live Photo)。

系统相册的操作是长按实况图 (Live Photo) 就会播放; 看了下微信的效果,预览的时候。自动播放,这个时候可以手势进行缩放,播放完毕,回到图片状态,保持缩放状态。说实话,做起来应该不难。

开干

图片手势的原理,是通过监听手势,去影响图片最终的绘制区域。所以说要做到视频(任何 Widget)也跟随手势变化,其实只用把手势处理的过程复制一份就好了,然后把结果给视频(任何 Widget),让它绘制到给定区域即可。

原理简读

  Widget _buildVideo(ExtendedImageGestureState? imageGestureState) {
    
    
    
    final Size size = MediaQuery.of(context).size;

    final Rect destinationRect = widget.buildWithImageRect
        ? GestureWidgetDelegateFromState.getRectFormState(
            Offset.zero & size,
            imageGestureState!,
          )
        : GestureWidgetDelegateFromState.getRectFormState(
            Offset.zero & size,
            imageGestureState!,
            width: _controller.value.size.width,
            height: _controller.value.size.height,
          );
    final ExtendedImageSlidePageState? extendedImageSlidePageState =
        imageGestureState.extendedImageSlidePageState;

    Widget child = VideoPlayer(_controller);

    if (widget.buildWithImageRect) {
      final double aspectRatio = widget.state.extendedImageInfo!.image.width /
          widget.state.extendedImageInfo!.image.height;

      if ((_controller.value.aspectRatio - aspectRatio).abs() > 0.01) {
        final Rect widgetDestinationRect =
            GestureWidgetDelegateFromState.getRectFormState(
          Offset.zero & size,
          imageGestureState,
          width: _controller.value.size.width,
          height: _controller.value.size.height,
          copy: true,
        );
        child = FittedBox(
          child: SizedBox(
            child: child,
            width: widgetDestinationRect.width,
            height: widgetDestinationRect.height,
          ),
          fit: BoxFit.cover,
          clipBehavior: Clip.hardEdge,
        );
      }
    }

    child = CustomSingleChildLayout(
      delegate: GestureWidgetDelegateFromRect(
        destinationRect,
      ),
      child: child,
    );

    
    
    
    
    
    
    
    
    

    if (extendedImageSlidePageState != null) {
      child = imageGestureState
              .widget.extendedImageState.imageWidget.heroBuilderForSlidingPage
              ?.call(child) ??
          child;
      if (extendedImageSlidePageState.widget.slideType == SlideType.onlyImage) {
        child = Transform.translate(
          offset: extendedImageSlidePageState.offset,
          child: Transform.scale(
            scale: extendedImageSlidePageState.scale,
            child: child,
          ),
        );
      }
    }

    return child;
  }
获取区域

rect 即图片占用的区域,例子里面是整个页面,你也可以通过 LayoutBuilder 去获取实际的区域

计算出绘制区域

可以根据你自身的需求,如果需要视频(任何 Widget)按照自身的宽高来绘制,那么在 GestureWidgetDelegateFromState.getRectFormState 方法调用的时候传入实际的宽高。

该方法实现为:

  static Rect getRectFormState(
    Rect rect,
    ExtendedImageGestureState state, {
    double? width,
    double? height,
    BoxFit? fit,
    bool copy = false,
  }) {
    final GestureDetails? gestureDetails = state.gestureDetails;

    if (gestureDetails != null && gestureDetails.slidePageOffset != null) {
      rect = rect.shift(-gestureDetails.slidePageOffset!);
    }

    Rect destinationRect = getDestinationRect(
      rect: rect,
      inputSize: Size(
        width ??
            state.widget.extendedImageState.extendedImageInfo!.image.width
                .toDouble(),
        height ??
            state.widget.extendedImageState.extendedImageInfo!.image.height
                .toDouble(),
      ),
      fit: fit ?? state.widget.extendedImageState.imageWidget.fit,
    );

    if (gestureDetails != null) {
      GestureDetails gd = gestureDetails;
      if (copy) {
        gd = gestureDetails.copy();
      }
      destinationRect = gd.calculateFinalDestinationRect(rect, destinationRect);

      if (gd.slidePageOffset != null) {
        destinationRect = destinationRect.shift(gd.slidePageOffset!);
      }
    }
    return destinationRect;
  }
}
  • 初始的绘制区域,首先要移除滑动退出的影响
  • getDestinationRect 方法根据绘制的区域大小和图片的大小(或者我们给定的视频(任何 Widget)的实际宽高)以及 BoxFit,来计算出来应该将视频(任何 Widget)绘制到什么区域。
  • GestureDetails 根据的缩放值,平移值等参数,计算出来,缩放平移后的视频(任何 Widget)绘制区域。
  • 还原滑动退出的影响
处理宽高比不近似相等
  • 当视频(任何 Widget)按照图片的宽高计算的时候,要注意它和图片宽高比。如果近似不相同,并且不做任何处理的话,视频(任何 Widget)会被压缩拉伸。

通过下面的方法,我们可以视频(任何 Widget)进行 conver 操作,使两者显示更自然。

        final Rect widgetDestinationRect =
            GestureWidgetDelegateFromState.getRectFormState(
          Offset.zero & size,
          imageGestureState,
          width: _controller.value.size.width,
          height: _controller.value.size.height,
          copy: true,
        );
        child = FittedBox(
          child: SizedBox(
            child: child,
            width: widgetDestinationRect.width,
            height: widgetDestinationRect.height,
          ),
          fit: BoxFit.cover,
          clipBehavior: Clip.hardEdge,
        );
处理滑动退出情况
  • 由于滑动退出可能变形,通过 heroBuilderForSlidingPage 进行修正。
  • 然后根据 slideType 的模式,对手势作用的 widget 进行变形。
应用最终绘制位置

最后一步把视频(任何 Widget) 绘制到处理之后的最终绘制区域,

当然,这是整个流程看起来很复杂,但是只有你需要自定义,你才需要关注它。

也提供了 wrapGestureWidget 方法,可以简单的处理整个过程(除了不同宽高比那部分)

  return imageGestureState!.wrapGestureWidget(
    VideoPlayer(_controller),
  );

上面只是怎么将缩放平移作用在视频(任何 Widget)的过程,其他细节还包括图片和视频(任何 Widget)切换动画,Live Photo 标志添加等细节,知道你们不喜欢看,直接给你们代码链接,有疑问的可以留言讨论。

完整代码: github.com/fluttercand…

优化细节

可能有这种需求,在用户做手势的时候或者滑动退出的时候,会根据情况对视频(任何 Widget) 特殊处理,比如在用户做手势的时候或者滑动退出的时候,停止视频播放,结束之后再继续播放。

滑动退出中

1726883654746.gif

可能用户会有需求,滑动退出过程中停止播放。

ExtendedImageSlidePageonSlidingPage 回调,你可以根据 ExtendedImageSlidePageState.isSliding 来判断,当前是否是在滑动退出手势中。

      ExtendedImageSlidePage(
        key: slidePagekey,
        onSlidingPage: (ExtendedImageSlidePageState state) {
          _isSliding.value = state.isSliding;
        },
      )
手势中

1726883654755.gif

可能用户会有需求,手势过程中停止播放。

GestureConfig 中有回调 gestureDetailsIsChanged, 可以通过该回调知道是否手势正在进行。

    ExtendedImage(
      image: image,
      fit: _fit,
      mode: ExtendedImageMode.gesture,
      enableSlideOutPage: true,
      initGestureConfigHandler: (ExtendedImageState state) {
        return GestureConfig(
          inPageView: true,
          initialScale: 1.0,
          maxScale: 5.0,
          animationMaxScale: 6.0,
          initialAlignment: InitialAlignment.center,
          gestureDetailsIsChanged: (GestureDetails? details) {
            _gestureDetailsIsChanging.value = true;
            _gestureDetailsChangeCompleted();
          },
        );
      },
    );

但是手势是有动画,而且没有结束的标志,所以我们需要利用 debounce 防抖,来判断手势是否结束掉了。意思就是如果 100 milliseconds 之后,这个方法不再触发,那么就认为手势已经结束。

  late VoidFunction _gestureDetailsChangeCompleted;

  @override
  void initState() {
    super.initState();

    _gestureDetailsChangeCompleted = () {
      _gestureDetailsIsChanging.value = false;
    }.debounce(const Duration(milliseconds: 100));
  }

手势结束,可能是用户手没有动了,即用户手指还是按压着的,只是没有变化。所以我们需要另外一个变量来优化这一场景。

    Listener(
      onPointerDown: (PointerDownEvent event) {
        _pointerDown = true;
      },
      onPointerUp: (PointerUpEvent event) {
        _pointerDown = false;

        SchedulerBinding.instance.addPostFrameCallback((_) {
          continuePlay();
        });
      },
      onPointerCancel: (PointerCancelEvent event) {
        _pointerDown = false;

        SchedulerBinding.instance.addPostFrameCallback((_) {
          continuePlay();
        });
      },
    );

如果用户手指没有抬起,那我们还是不要继续播放视频。

  Future<void> _onGestureDetailsIsChanged() async {
    if (!_showVideo.value) {
      return;
    }
    if (widget.gestureDetailsIsChanging.value) {
      await _controller.pause();
    } else if (!_pointerDown) {
      await continuePlay();
    }
  }

结语

完整的例子在:

extended_image/example/lib/pages/complex/live_photo_demo.dart at master · fluttercandies/extended_image (github.com)

wechat_assets_picker 已同步微信实况图效果。

1727261040094.gif

从最开始支持图片的缩放平移,就已经为后续功能铺好路,只要懂得其原理,一切都是水到渠成。

最后想说的是,年轻人还是不要太卷了,如果提前做了,今年的 kpi 又怎么完成呢? 微信怎么可以做?微信都不支持! 同理。

接下来的 kpi :

这只是饼,有可能完成。

Flutter,爱糖果,欢迎加入Flutter Candies,一起生产可爱的Flutter小糖果QQ群:181398081

最最后放上 Flutter Candies 全家桶,真香。

阅读全文
下载说明:
1、本站所有资源均从互联网上收集整理而来,仅供学习交流之用,因此不包含技术服务请大家谅解!
2、本站不提供任何实质性的付费和支付资源,所有需要积分下载的资源均为网站运营赞助费用或者线下劳务费用!
3、本站所有资源仅用于学习及研究使用,您必须在下载后的24小时内删除所下载资源,切勿用于商业用途,否则由此引发的法律纠纷及连带责任本站和发布者概不承担!
4、本站站内提供的所有可下载资源,本站保证未做任何负面改动(不包含修复bug和完善功能等正面优化或二次开发),但本站不保证资源的准确性、安全性和完整性,用户下载后自行斟酌,我们以交流学习为目的,并不是所有的源码都100%无错或无bug!如有链接无法下载、失效或广告,请联系客服处理!
5、本站资源除标明原创外均来自网络整理,版权归原作者或本站特约原创作者所有,如侵犯到您的合法权益,请立即告知本站,本站将及时予与删除并致以最深的歉意!
6、如果您也有好的资源或教程,您可以投稿发布,成功分享后有站币奖励和额外收入!
7、如果您喜欢该资源,请支持官方正版资源,以得到更好的正版服务!
8、请您认真阅读上述内容,注册本站用户或下载本站资源即您同意上述内容!
原文链接:https://www.dandroid.cn/22034,转载请注明出处。
0

评论0

显示验证码
没有账号?注册  忘记密码?