相关阅读
是不是你们最喜欢看的?这是一段有声音的 Gif ,流口水声。
前言
最近微信朋友圈可以发实况图了
,上了热搜!
我在想,好家伙, 实况图 (Live Photo
) iOS 9.1
就支持了,现在都 iOS 18 了。
我记得 photo_manager 和 wechat_assets_picker 很早就支持了呀!
虽然但是,然后需求它又来了。
微信都不支持,不要太卷了,年轻人。
什么是 实况图 (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
导出之后,是 2
个文件,一个是图片,一个是视频。
当然,你也可以直接使用 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
) 特殊处理,比如在用户做手势的时候或者滑动退出的时候,停止视频播放,结束之后再继续播放。
滑动退出中
可能用户会有需求,滑动退出过程中停止播放。
ExtendedImageSlidePage
有 onSlidingPage
回调,你可以根据 ExtendedImageSlidePageState.isSliding
来判断,当前是否是在滑动退出手势中。
ExtendedImageSlidePage(
key: slidePagekey,
onSlidingPage: (ExtendedImageSlidePageState state) {
_isSliding.value = state.isSliding;
},
)
手势中
可能用户会有需求,手势过程中停止播放。
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();
}
}
结语
完整的例子在:
wechat_assets_picker 已同步微信实况图效果。
从最开始支持图片的缩放平移,就已经为后续功能铺好路,只要懂得其原理,一切都是水到渠成。
最后想说的是,年轻人还是不要太卷了,如果提前做了,今年的 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