仿抖音发布按住拍呼吸效果

Android动画和Transition系列文章

抖音——时下最火的app之一,发布过程中有个按住拍的呼吸效果,效果如下所示:
仿动因发布按住拍呼吸效果
上面两个按钮,都是采用属性动画进行控制的,但实现细节稍有不同,左上采用的是StateListAnimator,只需要考虑跟随手指动就可以了;右下是在onTouch里面控制动画开启or关闭。
demo采用了自定义View的方式,重点有几点:

  1. 属性动画,定义View不具备的属性——内部半径参数
  2. onTouch(),跟随手指随动
  3. StateListAnimator,按照View不同状态执行不同动画。

代码实现

跟随手指随动

var myX: Float = 0f
    var myY: Float = 0f
    var xDiff: Float = 0f
    var yDiff: Float = 0f

    override fun onTouchEvent(event: MotionEvent?): Boolean {
        when (event?.action) {
            MotionEvent.ACTION_DOWN -> {
                myX = x
                myY = y
                xDiff = event?.rawX - x
                yDiff = event?.rawY - y
            }
            MotionEvent.ACTION_MOVE -> {
                x = event?.rawX - xDiff
                y = event?.rawY - yDiff
            }
            MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
                //回到最初的位置
                animate().x(myX).y(myY)
            }
        }
        return super.onTouchEvent(event)
    }

手指按下时,记录View的x、y值,move时改变x和y值,松开时,回到最初的位置。

自定义属性

var innerRaduisFactor: Float = 0f
        //内部半径因子,[0,1)
        set(value) {
            field = value
            invalidate()
        }
        
val showText = "按住拍"

    override fun onDraw(canvas: Canvas?) {
        super.onDraw(canvas)
        if (innerRaduisFactor >= MIN_INNER_RADIUS_FACTOR) {
            paint.style = Paint.Style.STROKE
            val left = measuredWidth * (1 - innerRaduisFactor) / 2
            val right = left + (width / 2 - left) * 2
            paint.strokeWidth = measuredWidth * (1 - innerRaduisFactor) / 2
            canvas?.drawArc(left, left, right, right, 0f, 360f, false, paint)
        } else {
            paint.style = Paint.Style.FILL
            canvas?.drawCircle(width / 2.0f, height / 2.0f, width / 2.0f, paint)
            paint.textSize = 40f
            paint.color = Color.WHITE
            val textX = (width - paint.measureText(showText)) / 2.0f
            val textY = height / 2 + Math.abs(paint.fontMetrics.ascent + paint.fontMetrics.descent) / 2
            canvas?.drawText(showText, 0, showText.length, textX, textY, paint)
            paint.color = Color.RED
        }
    }

innerRadiusFactor设置时,调用invalidate触发onDraw(),这样属性动画就可以使用innerRadiusFactor属性了。

StateListAnimator实现方式

关于StateListAnimator,可以参考让View跟随状态动起来——StateListAnimator。本例子中的代码主要是在state_pressed=true状态下,尺寸扩大1.5,然后innerRadiusFactor在[0.75,0.9]之间无限回荡;在state_pressed=false状态下,恢复到最初状态。

val breathAnimator: StateListAnimator by lazy {
        val pressedOuterAnim = AnimatorSet().apply {
            play(ObjectAnimator.ofFloat(this@BreathView, SCALE_X, 1.0f, 1.5f))
                    .with(ObjectAnimator.ofFloat(this@BreathView, SCALE_Y, 1.0f, 1.5f))
        }
        val pressedInnerAnim = ObjectAnimator.ofFloat(this, "innerRaduisFactor", MAX_INNER_RADIUS_FACTOR, MIN_INNER_RADIUS_FACTOR).apply {
            repeatMode = ValueAnimator.REVERSE
            repeatCount = ValueAnimator.INFINITE
            duration = 1000
        }
        val pressedAnim = AnimatorSet().apply {
            play(pressedOuterAnim).before(pressedInnerAnim)
        }
        val normalOuterAnim = AnimatorSet().apply {
            play(ObjectAnimator.ofFloat(this@BreathView, SCALE_X, 1.0f))
                    .with(ObjectAnimator.ofFloat(this@BreathView, SCALE_Y, 1.0f))
        }
        val normalInnerAnim = ObjectAnimator.ofFloat(this, "innerRaduisFactor", 0f)
        val normalAnim = AnimatorSet().apply {
            play(normalOuterAnim).before(normalInnerAnim)
        }
        StateListAnimator().apply {
            addState(intArrayOf(android.R.attr.state_pressed), pressedAnim)
            addState(intArrayOf(-android.R.attr.state_pressed), normalAnim)
        }
    }

    init {
        isClickable = true
        stateListAnimator = breathAnimator
    }

在init{}中给View设置stateListAnimator,需要注意的是isClickable必须得设为true,不然移动和动画都是无效的。

onTouch()方法中控制动画的开启与关闭

首先依然是定义属性动画,这个和StateListAnimator类似的,只不过那里是将两个动画组合在了一起,这里需要拆分开来。然后在onTouch()方法里面开启和关闭,onTouch()方法如下:

override fun onTouchEvent(event: MotionEvent?): Boolean {
        when (event?.action) {
            MotionEvent.ACTION_DOWN -> {
                myX = x
                myY = y
                xDiff = event?.rawX - x
                yDiff = event?.rawY - y
                normalAnimator.cancel()
                pressedAnimtor.start()
            }
            MotionEvent.ACTION_MOVE -> {
                x = event?.rawX - xDiff
                y = event?.rawY - yDiff
            }
            MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
                animate().x(myX).y(myY)
                pressedAnimtor.cancel()
                normalAnimator.start()
            }
        }
        return super.onTouchEvent(event)
    }

在down事件中开启innerRadiusFactor缩放的动画,在up或cancel事件中关闭动画。
另外需要注意的是,在onDetachFromWindow()中关闭动画,

override fun onDetachedFromWindow() {
        super.onDetachedFromWindow()
        pressedAnimtor.cancel()
        normalAnimator.cancel()
    }

总结

以上就是我这边想到的两种方式实现,其实本质都还是属性动画。
关于代码,可以参考BreathViewBreathView2
实际上,BreathView可以做的更精细化些,比如加入一些自定义属性,这样可定制更高些,这里只是为了模仿抖音的效果,因此就没有做的很细致。感兴趣的朋友可以对其做的精细化些。

参考

关注我的技术公众号,不定期会有技术文章推送,不敢说优质,但至少是我自己的学习心得。微信扫一扫下方二维码即可关注:
二维码

<think>好的,我现在需要帮助用户解决在Flutter中实现风格视频录制与上传功能的问题。用户之前已经询问过图片上传和裁剪,现在转向视频录制和上传,可能需要结合之前的经验。首先,我要回忆一下用户提供的引用内容,但根据系统提示,用户的上一次问题和回答更重要。用户提到想模仿的视频录制功能,包括按下录制、抬起暂停,断点显示进度条,以及视频压缩和上传。 首先,核心功能点是什么?的录制通常有:按住录制、暂停继续、分段进度条、视频合成、压缩和上传。Flutter需要选择合适的插件。用户之前的回答用了image_picker和image_cropper,但视频需要不同的插件。 视频录制方面,Flutter的插件可能有camera和video_player,但需要更高级的功能,比如分段录制。可能需要参考用户提供的引用中的Android原生实现,比如GitHub上的VideoRecorder项目,支持按下录制、抬起暂停,断点显示。但在Flutter中可能需要找类似的插件或自己实现。 视频压缩方面,用户引用了FFmpeg和MediaCodec的例子。Flutter中可以使用flutter_ffmpeg插件来处理视频压缩,或者寻找其他压缩库。上传部分可能需要使用http或dio库,类似之前图片上传的逻辑。 接下来,步骤应该是:添加依赖、配置权限、实现UI和录制逻辑、分段处理、压缩、上传。需要注意多平台适配,尤其是iOS和Android的权限和配置。 然后,考虑如何实现按住录制和暂停。可能需要监听手势事件,比如onLongPressStart开始录制,onLongPressEnd暂停。进度条部分需要态生成断点,可能需要自定义widget,根据录制的分段来显示。 视频合成方面,可能需要将多个视频文件合并成一个,这可以通过FFmpeg命令处理。压缩同样使用FFmpeg,调整参数如码率、分辨率。 上传部分需要分块上传或整体上传,显示进度条,处理可能的失败和重试。 另外,用户之前的回答中提到了image_cropper的多平台适配,可能需要类似的处理方式,确保视频录制和压缩在不同平台上正常工作。权限配置方面,相机、麦克风、存储权限都需要在Android和iOS中配置。 需要检查是否有现成的Flutter插件支持分段录制,或者是否需要结合原生代码。例如,camera插件可能提供基础的录制功能,但分段需要自己管理。或者使用community维护的插件,如flutter_video_recorder,但需要确认其功能是否支持。 如果现有插件不支持,可能需要自己编写平台特定的代码,通过MethodChannel调用原生功能,类似引用中的Android项目。不过这样会增加复杂度,可能用户希望尽量用Flutter实现。 最后,确保代码示例正确,包括依赖版本、权限设置,以及处理可能的异常,比如权限被拒绝、存储空间不足等。同时,性能优化也是重点,比如视频压缩的速度和效果,引用中提到的MediaCodec和FFmpeg各有优劣,需要根据需求选择。 总结步骤:选择插件(camera、video_compress、http/dio),配置权限,实现录制逻辑(分段、进度条),合成与压缩,上传服务器。需要详细说明每个步骤,并给出代码示例,同时解答相关问题。</think>在Flutter中实现风格视频录制与上传功能,需结合视频分段录制、压缩处理和分块上传技术。以下是完整实现方案: --- ### 一、核心插件选择 1. **视频录制**:`camera`(官方插件)+ `flutter_video_recorder`(分段录制) 2. **视频压缩**:`video_compress`(支持FFmpeg内核) 3. **文件上传**:`dio`(支持分块上传) 4. **手势控制**:`GestureDetector`原生组件 ```yaml dependencies: camera: ^0.10.0+1 flutter_video_recorder: ^2.0.0 video_compress: ^3.1.0 dio: ^5.0.0 ``` --- ### 二、关键功能实现 #### 1. 分段录制控制(仿按压逻辑) ```dart bool _isRecording = false; List<String> _videoSegments = []; GestureDetector( onLongPressStart: (_) => _startRecording(), onLongPressEnd: (_) => _pauseRecording(), child: Container( width: 80, height: 80, decoration: BoxDecoration( border: Border.all(color: Colors.white, width: 4), shape: BoxShape.circle ), ), ) // 开始/暂停录制 Future<void> _startRecording() async { if (!_isRecording) { final file = await FlutterVideoRecorder.startRecording(); _videoSegments.add(file.path); _isRecording = true; } } Future<void> _pauseRecording() async { await FlutterVideoRecorder.pauseRecording(); _isRecording = false; } ``` #### 2. 态进度条实现 ```dart // 进度条UI构建 Widget _buildProgressBar() { return Container( height: 4, child: ListView.builder( scrollDirection: Axis.horizontal, itemCount: _videoSegments.length, itemBuilder: (ctx, index) => Container( width: MediaQuery.of(context).size.width / 15, margin: EdgeInsets.symmetric(horizontal: 2), color: Colors.redAccent, ), ), ); } ``` #### 3. 视频合成与压缩 ```dart // 合并多个分段视频 Future<File> _mergeVideos() async { final mergedPath = await VideoCompress.mergeSegments( _videoSegments, outputFormat: VideoFormat.mp4, ); return File(mergedPath); } // 压缩视频(保留高清画质) Future<File> _compressVideo(File original) async { final MediaInfo info = await VideoCompress.compressVideo( original.path, quality: VideoQuality.MediumQuality, // 平衡画质与体积 deleteOrigin: false, ); return File(info.path); } ``` --- ### 三、分块上传实现 ```dart Future<void> _uploadVideo(File video) async { final fileSize = video.lengthSync(); const chunkSize = 5 * 1024 * 1024; // 5MB分块 final dio = Dio(); int uploaded = 0; for (int start = 0; start < fileSize; start += chunkSize) { final end = (start + chunkSize) > fileSize ? fileSize : start + chunkSize; final bytes = await video.readAsBytes().then((b) => b.sublist(start, end)); final formData = FormData.fromMap({ 'file': MultipartFile.fromBytes(bytes, filename: 'chunk_${start ~/ chunkSize}'), 'totalChunks': (fileSize / chunkSize).ceil(), 'currentChunk': start ~/ chunkSize, }); await dio.post( 'https://api.example.com/upload', data: formData, onSendProgress: (sent, total) { print('上传进度: ${((uploaded + sent)/fileSize*100).toStringAsFixed(1)}%'); }, ); uploaded += end - start; } } ``` --- ### 四、平台适配要点 #### Android配置 ```xml <!-- android/app/src/main/AndroidManifest.xml --> <uses-permission android:name="android.permission.CAMERA"/> <uses-permission android:name="android.permission.RECORD_AUDIO"/> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> ``` #### iOS配置 ```xml <!-- ios/Runner/Info.plist --> <key>NSCameraUsageDescription</key> <string>需要相机权限摄视频</string> <key>NSMicrophoneUsageDescription</key> <string>需要麦克风权限录制声</string> <key>NSPhotoLibraryAddUsageDescription</key> <string>需要相册权限保存视频</string> ``` --- ### 五、性能优化建议 1. **内存管理**:分段录制时及时释放已完成片段的内存 2. **压缩策略**:根据网络条件态调整压缩率[^1] 3. **断点续传**:记录上传进度实现断点续传功能[^2] 4. **预览优化**:使用`chewie`插件实现流畅预览 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值