最近 Keep 更新了一个轨迹动画的分享,可以生成一个看起来高大上的视频。项目产品也提出了一样的需求,便参照着实现了一波。效果如下(虚拟机跑动画就已经很卡了,然后还要录 gif 就更卡了,凑合看~):
整体思路就是:
- Android 原生播放动画(基于高德地图实现)
- 录制屏幕生成视频
播放动画无非就是一次画一条线,连起来就像一个点一直在往前面爬的效果了。然后请求录制屏幕,输出到一个文件,最后保存为视频即可。
Android 录屏核心思路是使用系统 Api MediaProjectionManager,需要 Android 5.0 以上才可以使用。鉴于目前市场 Android 5.0 的手机已经很少了,所以便直接使用 MediaProjectionManager 了。
使用步骤:
- mMediaProjectionManager.createScreenCaptureIntent() 申请录屏权限(因为要保存文件所以需要申请 WRITE_EXTERNAL_STORAGE 权限)
- 用户同意之后,初始化 VirtualDisplay
- 创建 MediaRecorder,设置好相关参数,核心设置视频源为 SURFACE: setVideoSource(MediaRecorder.VideoSource.SURFACE)
- 录制结束后释放资源,保存文件
具体代码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148/**
* 录屏帮助类,仅限 Android 5.0 及以上使用
*/
class ScreenRecorder(val activity: Activity, val listener: VideoRecordListener?, private val saveName: String) {
private val REQUEST_MEDIA_PROJECTION_CODE = 1000
/**
* 录制视频的分辨率、比特率、帧率
*/
private var mVideoWidth: Int
private var mVideoHeight: Int
private val VIDEO_BIT_RATE = 12 * 1024 * 1024
private val VIDEO_FRAME_RATE = 60
private var mMediaProjectionManager = activity.getSystemService(Context.MEDIA_PROJECTION_SERVICE) as? MediaProjectionManager
private var mMediaRecorder: MediaRecorder? = null
private var mVirtualDisplay: VirtualDisplay? = null
private var mMetrics = DisplayMetrics()
private var mSaveFile: File? = null
init {
activity.windowManager.defaultDisplay.getMetrics(mMetrics)
mVideoWidth = mMetrics.widthPixels
mVideoHeight = mMetrics.heightPixels
}
/**
* 开始录制
*/
fun startRecord() {
PermissionUtils.requestPermissions(activity, PermissionsCallback {
if (it.grantedAll) {
listener?.beforeRecord()
StartForResult.from(activity).startForResult(mMediaProjectionManager!!.createScreenCaptureIntent(),
REQUEST_MEDIA_PROJECTION_CODE) { requestCode, resultCode, data ->
if (requestCode == REQUEST_MEDIA_PROJECTION_CODE && resultCode == Activity.RESULT_OK) {
initRecorder()
MainThreadUtils.postDelayed({
val mp = mMediaProjectionManager!!.getMediaProjection(resultCode, data)
mVirtualDisplay = mp.createVirtualDisplay("ScreenCapture", mVideoWidth, mVideoHeight, mMetrics.densityDpi,
DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR, mMediaRecorder?.surface, null, null)
mMediaRecorder?.start()
listener?.startRecord()
}, 300)
} else {
listener?.cancelRecord()
}
}
} else {
ToastUtils.toast("录屏需要写存储权限")
}
}, Manifest.permission.WRITE_EXTERNAL_STORAGE)
}
/**
* 录制中断,删除视频文件
*/
fun interruptRecord() {
releaseRecorder()
if (mSaveFile != null) {
ToastUtils.toast("视频保存失败")
}
mSaveFile?.delete()
mSaveFile = null
}
/**
* 结束录制
*/
fun finishRecord() {
releaseRecorder()
if (mSaveFile != null) {
val newFile = File(DirUtils.getPublicMediaPath(), "$saveName.mp4")
// 录制结束后修改后缀为 mp4
mSaveFile!!.renameTo(newFile)
val intent = Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE)
intent.data = Uri.fromFile(newFile)
MucangConfig.getContext().sendBroadcast(intent)
ToastUtils.toast("已保存到相册")
}
mSaveFile = null
}
/**
* 释放资源
*/
private fun releaseRecorder() {
mMediaRecorder?.stop()
mMediaRecorder?.release()
mMediaRecorder = null
mVirtualDisplay?.release()
mVirtualDisplay = null
}
/**
* 初始化 MediaRecorder
*/
private fun initRecorder() {
mSaveFile = File(DirUtils.getPublicMediaPath(), "$saveName.tmp")
if (mSaveFile!!.exists()) {
mSaveFile!!.delete()
}
mMediaRecorder = MediaRecorder()
mMediaRecorder?.setVideoSource(MediaRecorder.VideoSource.SURFACE)
mMediaRecorder?.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4)
mMediaRecorder?.setOutputFile(mSaveFile!!.absolutePath)
mMediaRecorder?.setVideoSize(mVideoWidth, mVideoHeight)
mMediaRecorder?.setVideoEncoder(MediaRecorder.VideoEncoder.H264)
mMediaRecorder?.setVideoEncodingBitRate(VIDEO_BIT_RATE)
mMediaRecorder?.setVideoFrameRate(VIDEO_FRAME_RATE)
try {
mMediaRecorder?.prepare()
} catch (e: IOException) {
LogUtils.e("ScreenRecorder", e.toString())
}
}
interface VideoRecordListener {
/**
* 录制开始时隐藏不必要的UI
*/
fun beforeRecord()
/**
* 开始录制
*/
fun startRecord()
/**
* 取消录制
*/
fun cancelRecord()
}
}