Google Play应用上架流程

注册开发者账号

创建应用

选择默认语言 (最好选择英语),填写应用名称

商品详情(默认选中)

需要填写 App简介和详细描述内容(需要和默认语言一致)

上传APP图片(屏幕截图、高分辨率图标、置顶大图)


选择应用类型和类别

填写公司官网,联系邮箱(邮箱地址会展示给用户,不要乱填),内容分级(可以先不管,在上传App后才能进行分级判断),电话(填写公司电话,会展示给用户),隐私权政策。

上传正式版应用(商品版本: 正式版、Beta版、Alpha版本)

创建版本

加入 Google Play App Signing

上传APK安装包

填写版本说明

内容分级

填写电子邮件地址(用来接收Google Play发送的消息),选择应用类别

调查问卷页 (基本上都选择否)点击 判断分级

确认分级

定价和分发范围

注意 “ 国家/地区 ” 默认是 0,需要自己选择国家

内容准则 和 美国出口法律 是必须要选中的,最后点击 “ 保存草稿 ”

应用发布

如果所有内容都填写正确,我们会看到左边导航栏4个绿色对勾,并且出现“可以发布”按钮

点击左边导航栏中的 " 应用版本 "

点击 “ 修改版本 ”

点击 “ 查看 ” , 确认没有问题后,点击 “ 开始发布正式版 ”,就可以发布了,然后就需要审核了

查看审核进度

FileUtils帮助类

AndroidUtilCode的基础下使用

  • 将assets文件夹中的内容写入本地
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* 将assets文件夹中的内容写入本地
* @param dirName String 文件夹名称
* @param fileName String 文件名称及后缀名
* @return String? SDCard/Android/data/应用包名/files/ 目录/文件
*/
private suspend fun getExternalFilePath(dirName: String, fileName: String): String? =
withContext(Dispatchers.IO) {
//创建文件夹
val mFileDir = Utils.getApp().applicationContext.getExternalFilesDir(dirName)
if (mFileDir != null) {
//创建文件
val mFilePath: String = File(mFileDir, fileName).absolutePath
if (FileUtils.isFileExists(mFilePath)) {
return@withContext mFilePath
//将assets文件夹中的内容写入本地
} else if (ResourceUtils.copyFileFromAssets(fileName, mFilePath)) {
return@withContext mFilePath
} else {
return@withContext null
}
}
return@withContext null
}

视频时长(Long)转字符串(String)_Java

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
/**
* 时长转字符
* @param mDuration 时长(毫秒)
* @param isMillisecond 是否需要毫秒,true:需要,false:不用
* @return
*/
public static String getTimeDurationString(@NonNull Long mDuration, @NonNull Boolean isMillisecond) {
Long hours = 0L, minutes = 0L, seconds = 0L, millisecond = null;
if (isMillisecond) {
//毫秒
millisecond = mDuration % TimeConstants.SEC;
}
hours = mDuration / TimeConstants.HOUR;
minutes = (mDuration / TimeConstants.MIN)-(hours*60);
seconds = (mDuration / TimeConstants.SEC)-(minutes*60)-(hours*3600);
String str_duration = getTimeString(hours, minutes, seconds, millisecond);
return str_duration;
}

private static String getTimeString(@NonNull Long hours, @NonNull Long minutes, @NonNull Long seconds, Long millisecond) {
if (millisecond != null) {
return String.format("%02d:%02d:%02d.%02d", hours, minutes, seconds, millisecond);
} else {
return String.format("%02d:%02d:%02d", hours, minutes, seconds);
}
}

FFmpeg常用命令

改变帧率,码率和文件大小

  • 帧率
1
2
//改变视频 input.mp4 的帧率,生成帧率为30的新视频 output.mp4
ffmpeg -i input.mp4 -r 30 output.mp4
  • 码率
1
2
//改变视频 input.mp4 的帧率,生成码率为 1.5 Mb/s(兆比特)的新视频 output.mp4
ffmpeg -i input.mp4 -b 1.5M output.mp4
  • 最大输出文件大小
1
2
//改变视频 input.mp4 的文件大小,生成文件大小为10MB的新视频 output.mp4
ffmpeg -i input.mp4 -fs 10MB output.mp4

调整视频分辨率

  • 分辨率:w*h (w:像素宽度,h:像素高度)
1
2
3
4
5
6
7
8
9
10
11
//改变视频 input.mp4 的分辨率,生成分辨率为320*240的新视频 output.mp4
ffmpeg -i input.mp4 -s 320*240 output.mp4

//改变视频 input.mp4 的分辨率(使用 scale filter 替代 -s ),生成分辨率为 320*240 的新视频 output.mp4
ffmpeg -i input.mp4 -vf scale=320:240 output.mp4

//改变视频 input.mp4 的分辨率,生成分辨率为640*480的新视频 output.mp4
ffmpeg -i input.mp4 -s 640*480 output.mp4

//改变视频 input.mp4 的分辨率,生成分辨率为 vga (640*480)的新视频 output.mp4
ffmpeg -i input.mp4 -s vga output.mp4

裁剪、填充视频

  • 视频裁剪

1
2
3
4
5
6
7
8
9
10
11
//裁剪矩形框---靠左1/3
ffmpeg -i input.mp4 -vf crop=iw/3:ih:0:0 output.mp4

//裁剪矩形框---靠中1/3
ffmpeg -i input.mp4 -vf crop=iw/3:ih:iw/3:0 output.mp4

//裁剪矩形框---靠右1/3
ffmpeg -i input.mp4 -vf crop=iw/3:ih:iw/3*2:0 output.mp4

//裁剪中间的一半(默认从输入视频的中间区域开始)
ffmpeg -i input.mp4 -vf crop=iw/2:ih/2 output.mp4
  • 视频填充

1
2
3
4
5
6
7
8
9
10
//改变视频的宽高比(4:3),生成宽高比为16:9的新视频 
ffmpeg -i input -vf pad=ih*16/9:ih:(ow-iw)/2:0:color output

//改变视频的宽高比(4:3),生成宽高比为16:9的新视频(颜色默认:黑色)
ffmpeg -i input -vf pad=ih*16/9:ih:(ow-iw)/2:0 output

//改变视频的宽高比(16:9),生成宽高比为4:3的新视频
ffmpeg -i input -vf pad=iw:iw*3/4:0:(oh-ih)/2:0:color output

ffmpeg -i input -vf pad=iw:iw*3/4:0:(oh-ih)/2:0 output

翻转、旋转视频

  • 视频翻转
1
2
3
4
5
//水平翻转
ffplay -f lavfi -i testsrc -vf hflip

//垂直翻转
ffplay -f lavfi -i testsrc -vf vflip
  • 视频旋转
    参数:transpose
    0 : 逆时针方向旋转90° 并且垂直翻转
    1 : 顺时针方向旋转90°
    2 : 逆时针方向旋转90°
    3 : 顺时针方向旋转90° 并且垂直翻转
1
2
3
4
5
6
7
8
9
10
11
//逆时针方向旋转90° 并且垂直翻转
ffmpeg -i input.mp4 -vf transpose=0 output.mp4

//顺时针方向旋转90°
ffmpeg -i input.mp4 -vf transpose=1 output.mp4

//逆时针方向旋转90°
ffmpeg -i input.mp4 -vf transpose=2 output.mp4

//顺时针方向旋转90° 并且垂直翻转
ffmpeg -i input.mp4 -vf transpose=3 output.mp4

模糊、锐化视频

  • 模糊视频
1
2
3
4
5
//模糊视频 input.mp4 ,生成一个luma半径为1.5,luma权值为1的新视频 output.mp4
ffmpeg -i input.mp4 -vf boxblur=1.5:1 output.mp4

//模糊视频 input.mp4 ,生成一个luma半径为1,luma亮度为0.8,亮度阈值为0的新视频 output.mp4 (不影响图像的轮廓,使用smartblur滤波器处理)
ffmpeg -i input.mp4 -vf smartblur=5:0.8:0 output.mp4
  • 锐化视频 (unsharp 滤波器)
1
2
3
4
5
6
7
8
9
10
11
//锐化视频 input.mp4 ,生成一个新视频 output.mp4
ffmpeg -i input.mp4 -vf unsharp output.mp4

//锐化视频 input.mp4 ,生成一个 5*5的矩形luma 强度为 1 色度值为-2 的新视频 output.mp4
ffmpeg -i input.mp4 -vf unsharp=6:6:-2 output.mp4

//增强去噪 (denoise3d视频滤波器)
ffmpeg -i input.mp4 -vf mp=denoise3d output.mp4

//视频降噪 (hqdn3d过滤器)
ffmpeg -i input.mp4 -vf hqdn3d output.mp4

画中画

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
//视频叠加 input1:视频背景,input2:前景图
ffmpeg -i input1 -i input2 -filter_complex overlay=x:y output

ffmpeg -i input1 -vf movie=input2[logo];[in][logo]overlay=x:y output

//视频 input.mp4 添加logo到视频左上角,生成新视频 output.mp4
ffmpeg -i input.mp4 -i logo.png -filter_complex overlay output.mp4

//视频 input.mp4 添加logo到视频右上角,生成新视频 output.mp4
ffmpeg -i input.mp4 -i logo.png -filter_complex overlay=W-w output.mp4

//视频 input.mp4 添加logo到视频右下角,生成新视频 output.mp4
ffmpeg -i input.mp4 -i logo.png -filter_complex overlay=W-w:H-h output.mp4

//视频 input.mp4 添加logo到视频左下角,生成新视频 output.mp4
ffmpeg -i input.mp4 -i logo.png -filter_complex overlay=0:H-h output.mp4

//视频 input.mp4 添加logo(5s后显示),生成新视频 output.mp4
ffmpeg -i input.mp4 -itsoffset 5 -i logo.png -filter_complex overlay output.mp4

//视频 input.mp4 添加计时器,生成新视频 output.mp4

//生成计时器
ffmpeg -f lavfi -i testsrc -vf crop=61:52:224:94 -t 142 timer.ogg

//添加计时器到视频中
ffmpeg -i input.mp4 -i timer.ogg -filter_complex overlay=451 output.mp4

//添加计时器到视频中(将计时器缩小到原来的1/2,并放到底部)
ffmpeg -i input.mp4 -vf movie=timer.ogg,scale=15:14[tm];[in][tm] overlay=248:371 output.mp4

在视频上添加文字

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
//字体文件(arial.ttf)复制到当前目录下
ffplay -f lavfi -i color=c=white -vf drawtext=fontfile=arial.ttf:text=Welcome

//文字位置(x:水平方向,y:垂直方向)
//tw:文本宽度,w:帧宽,(w-tw)/2:水平居中,w-tw:文本对齐到右边
//th:文本高度,w:帧高,(h-th)/2:垂直居中,h-th:文本对齐到底部
//drawtext(滤镜):如果其中有空格,将包含在成对单引号或成对双引号中
ffplay -f lavfi -i color=c=white -vf drawtext="fontfile=arial.ttf:text='hello world':x=(w-tw)/2:y=(h-th)/2"

//fontcolor:文字颜色,fontsize:文字大小,color:背景颜色
//背景色:蓝色,字体颜色:黄色,字体大小:30
ffplay -f lavfi -i color=c=blue -vf drawtext="fontfile=arial.ttf:text='hello world':x=(w-tw)/2:y=(h-th)/2:fontcolor=yellow:fontsize=30"

//水平方向上的文字运动
//t:时间(单位:s),n:移动像素
//从右往左方向向上移动,每秒移动n个像素: x = w - t*n
//从左往右方向向上移动,每秒移动n个像素: x = w + t*n

//顶部移动
ffmpeg -f lavfi -i color=c=blue -vf drawtext="fontfile=arial.ttf:text='hello world':x=w-t*50:fontcolor=yellow:fontsize=30" output

//底部滚动
ffmpeg -f lavfi -i color=c=blue -vf drawtext="fontfile=arial.ttf:textfile=info.txt:x=w-t*50:y=h-th:fontcolor=yellow:fontsize=30" output

//垂直方向文字滚动
ffmpeg -i input.mp4 -vf drawtext="fontfile=arial.ttf:textfile=Credits:x=(w-tw)/2:y=h-t*100:fontcolor=white:fontsize=30" output.mp4

视频文件格式转化

格式(视频) 编解码器 其他数据
.avi mpeg4 mpeg4 (Simple profile), yuv420p; audio: mp3
.flv flv1 yuv420p; audio: mp3
.mkv h264 h264 (High), yuvj420p; audio: vorbis codec, fltp sample format
.mov h264 h264 (High), yuvj420p; audio: aac (mp4a)
.mp4 h264 h264 (High), yuvj420p; audio: aac (mp4a)
.mpg mpeg1video yuv420p; audio: mp2
.ogg theora yuv422p, bitrate very low; audio excluded during conversion
.ts mpeg2video yuv422p; audio: mp2
.webm vp8 yuv420p; audio: vorbis codec, fltp sample format
格式(音频) 编解码器 其他数据
.aac aac libvo_aacenc, bitrate 128 kb/s
.flac flac FLAC (Free Lossless Audio Codec), bitrate 128 kb/s
.m4a aac mp4a, bitrate 128 kb/s
.mp2 mp2 MPEG Audio Layer 2, bitrate 128 kb/s
.mp3 mp3 libmp3lame, bitrate 128 kb/s
.wav pcm_s16le PCM (Pulse Code Modulation), uncompressed
.wma wmav2 Windows Media Audio
  • 重写相同命名的输出文件
1
ffmpeg -y -i input.avi output.mp4

时间操作

1
2
3
4
5
6
7
8
9
10
11
12
13
//通过视频 input.mp4 ,生成一个时长为120s的新视频 output.mp4
ffmpeg -i input.mp4 -t 120 output.mp4

//通过视频 input.mp4 ,生成一个时长为30s帧率为25的新视频 output.mp4
ffmpeg -i input.mp4 -t 30 -r 25 output.mp4

//设置一个10分钟25fps的视频
ffmpeg -i video.avi -vframes 15000 video_10_minute.avi

//设置延迟转换 (从10s开始进行转换)
ffmpeg -i input.avi -ss 10 output.mp4
//提取媒体文件中部分内容 (提取视频第5分钟的内容)
ffmpeg -i video.avi -ss 240 -t 60 clip_5th_minute.mpg

flutter练习 - Future与FutureBuilder实例

Future 异步操作

  • then :异步操作逻辑
  • whenComplete :异步完成时回调
  • catchError :捕获异常或异步出错时的回调
  • timeout :设置超时时间
Future的then的原型
1
Future<R> then<R>(FutureOr<R> onValue(T value),{Function onError});
  • onValue :成功的结果回调
  • onError :可选 ,执行出现异常
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import 'dart:async';
Future<String> testFuture() {
// throw Error();
return Future.value("success");
// return Future.error("error");
}
//注意: 如果 catchError 与 onError 同时存在时,则只会调用 onError
void main() {
testFuture().then((s) {
print("then : " + s);
}, onError: (e) {
print("onError : " + e);
}).whenComplete(() {
print("whenComplete");
}).catchError((e) {
print("catchError : " + e);
});
}

结合 async, await
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import 'dart:async';

test() async {
String result = await Future.delayed(Duration(milliseconds: 2000), () {
return Future.value("success");
});
print("t3 : " + DateTime.now().toString());
print("result : " + result);
}

main() {
test();
print("t1 : " + DateTime.now().toString());
print("t2 : " + DateTime.now().toString());
}

future.timeout 设置超时时间
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import 'dart:async';

void main() {
Future.delayed(Duration(seconds: 2), () {
return "success";
}).timeout(Duration(seconds: 1)).then((s) {
print("then : $s");
}).whenComplete(() {
print("whenComplete");
}).catchError((e) {
print("catchError : $e");
});
}

代码运行会出现如下结果:

1
2
whenComplete
catchError : TimeoutException after 0:00:01.000000: Future not completed

FutureBuilder ---- 网络请求,数据库读取 更新 界面

  • FutureBuildere是一个将异步操作和异步UI更新结合在一起的类,通过它我们可以将网络请求,数据库读取等的结果更新到页面上
  • FutureBuilder的构造方法
1
FutureBuilder({Key key,Future<T> future,T initialData,@required AsyncWidgetBuilder<T>builder});
  • future :Future对象表示此构造器当前连接的异步计算
  • initialData :表示一个非空的Future完成前的初始化数据
  • builder :AsyncWidgetBuilder类型返回的函数,是一个基于异步交互构建widget的函数
    builder:
    BuildContext context
    AsyncSnapshot snapshot
    connectionState 表示与异步计算的连接状态
    data - 异步计算接收的最新数据
    error - 异步计算接收的最新错误对象;
ConnectionState :与异步计算的连接状态
  • none :future还未执行的快照状态
  • waiting :连接到一个异步操作,并且等待交互,一般在这种状态的时候,我们可以显示加载框
  • active :连接到一个活跃的操作,比如stream流,会不断地返回值,并还没有结束,一般也是可以显示加载框
  • done:异步操作执行结束,一般在这里可以去拿取异步操作执行的结果,并显示相应的布局
ConnectionState 当前没有连接到任何的异步任务
ConnectionState.none 当前没有连接到任何的异步任务
ConnectionState.waiting 连接到异步任务并等待进行交互
ConnectionState.active 连接到异步任务并开始交互
ConnectionState.done 异步任务中止
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
import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;

void main() => runApp(MyApp1());

class MyApp1 extends StatefulWidget {
@override
_MyApp1State createState() => _MyApp1State();
}

class _MyApp1State extends State<MyApp1> {
String showResult = '';

Future<CommonModel> fetchPost() async {
final response = await http
.get('http://www.devio.org/io/flutter_app/json/test_common_model.json');
Utf8Decoder utf8decoder = Utf8Decoder(); //中文乱码
// final result=json.decode(response.body);
final result = json.decode(utf8decoder.convert(response.bodyBytes));
return CommonModel.fromJson(result);
}

@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: Text('FutureBuilder')),
body: FutureBuilder<CommonModel>(
future: fetchPost(),
builder: (BuildContext context, AsyncSnapshot<CommonModel> snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.none:
return new Text('Input a URL to start');
case ConnectionState.waiting:
return new Center(child: new CircularProgressIndicator());
case ConnectionState.active:
return new Text('');
case ConnectionState.done:
if (snapshot.hasError) {
return new Text(
'${snapshot.error}',
style: TextStyle(color: Colors.red),
);
} else {
return new Column(children: <Widget>[
Text('icon:${snapshot.data.icon}'),
Text('statusBarColor:${snapshot.data.statusBarColor}'),
Text('title:${snapshot.data.title}'),
Text('url:${snapshot.data.url}')
]);
}
}
},
),
),
);
}
}

class CommonModel {
final String icon;
final String title;
final String url;
final String statusBarColor;
final bool hideAppBar;

CommonModel(
{this.icon, this.title, this.url, this.statusBarColor, this.hideAppBar});

factory CommonModel.fromJson(Map<String, dynamic> json) {
return CommonModel(
icon: json['icon'],
title: json['title'],
url: json['url'],
statusBarColor: json['statusBarColor'],
hideAppBar: json['hideAppBar'],
);
}
}