本次项目实训目标打造一款智能视频剪辑APP,所提供的基本功能为:由用户选择本地视频进行上传,并且提供希望剪辑的主要人物图像,系统将智能检测出人物所在视频片段,并且合成人物cut视频,用户自行选择下载。
在本次实训中,我所负责的部分是移动端代码的编程,本周主要完成的工作是项目基本架构的搭建。
项目意图打造一款 Android 端app,移动端目录结构基本如下:
项目中主要使用了X系列的框架,包括:
XUI | 一个简洁的Android原生UI框架,包括UI组件、UI主题样式、UI资源和UI辅助工具 |
XPage | 一个方便的fragment页面框架,以通用的Activity作为壳,Fragment作为页面填充展示,并且能够像Activity那样自由的切换和数据交互 |
XRouter | 一个轻量级的Android路由框架,基于ARouter上进行改良,优化Fragment的使用,可结合XPage使用 |
XHttp2 | 一个功能强大的网络请求库,使用RxJava2 + Retrofit2 + OKHttp组合进行封装。支持多种Http请求方式,提供大量丰富的网络请求拦截器 |
XUtil | 一个方便实用的Android工具类库。收录了Android开发过程中常用的工具类,并进行简单的分类,易于查询使用 |
XUpdate | 一个轻量级、高可用性的Android版本更新框架 |
XAOP | 一个轻量级的AOP(面向切片编程)应用框架 |
创建 x-library.gradle 文件,以引入X系列框架,并将X-Library依赖添加到 build.gradle 文件中
apply plugin: 'com.xuexiang.xaop' //引用XAOP插件
apply plugin: 'com.xuexiang.xrouter' //引用XRouter-plugin插件实现自动注册
//自动添加依赖
configurations.each { configuration ->
def dependencies = getProject().dependencies
if (configuration.name == "implementation") {
//为Project加入X-Library依赖
//XUI框架
configuration.dependencies.add(dependencies.create(deps.xlibrary.xui))
configuration.dependencies.add(dependencies.create(deps.androidx.appcompat))
configuration.dependencies.add(dependencies.create(deps.androidx.recyclerview))
configuration.dependencies.add(dependencies.create(deps.androidx.design))
configuration.dependencies.add(dependencies.create(deps.glide))
//XUtil工具类
configuration.dependencies.add(dependencies.create(deps.xlibrary.xutil_core))
//XAOP切片
configuration.dependencies.add(dependencies.create(deps.xlibrary.xaop_runtime))
//XUpdate版本更新
configuration.dependencies.add(dependencies.create(deps.xlibrary.xupdate))
//XHttp2
configuration.dependencies.add(dependencies.create(deps.xlibrary.xhttp2))
configuration.dependencies.add(dependencies.create(deps.rxjava2))
configuration.dependencies.add(dependencies.create(deps.rxandroid))
configuration.dependencies.add(dependencies.create(deps.okhttp3))
configuration.dependencies.add(dependencies.create(deps.gson))
//XPage
configuration.dependencies.add(dependencies.create(deps.xlibrary.xpage_lib))
//页面路由
configuration.dependencies.add(dependencies.create(deps.xlibrary.xrouter_runtime))
}
if (configuration.name == "annotationProcessor") {
//XPage
configuration.dependencies.add(dependencies.create(deps.xlibrary.xpage_compiler))
//页面路由
configuration.dependencies.add(dependencies.create(deps.xlibrary.xrouter_compiler))
}
if (configuration.name == "debugImplementation") {
//内存泄漏监测leak
configuration.dependencies.add(dependencies.create(deps.leakcanary))
}
}
configurations.all {
resolutionStrategy.force deps.okhttp3
}
在 util 文件夹下,创建 XBasicLibInit.java 文件,来完成X系列基础库的初始化,在 MyApp.java 中调用 init 函数,使得项目运行即可初始化基础库
public final class XBasicLibInit {
private XBasicLibInit() {
throw new UnsupportedOperationException("u can't instantiate me...");
}
/**
* 初始化基础库SDK
*/
public static void init(Application application) {
//工具类
initXUtil(application);
//网络请求框架
initXHttp2(application);
//页面框架
initXPage(application);
//切片框架
initXAOP(application);
//UI框架
initXUI(application);
//路由框架
initRouter(application);
}
/**
* 初始化XUtil工具类
*/
private static void initXUtil(Application application) {
XUtil.init(application);
XUtil.debug(MyApp.isDebug());
TokenUtils.init(application);
}
/**
* 初始化XHttp2
*/
private static void initXHttp2(Application application) {
//初始化网络请求框架,必须首先执行
XHttpSDK.init(application);
//需要调试的时候执行
if (MyApp.isDebug()) {
XHttpSDK.debug();
}
// XHttpSDK.debug(new CustomLoggingInterceptor()); //设置自定义的日志打印拦截器
//设置网络请求的全局基础地址
XHttpSDK.setBaseUrl("https://gitee.com/");
// //设置动态参数添加拦截器
// XHttpSDK.addInterceptor(new CustomDynamicInterceptor());
// //请求失效校验拦截器
// XHttpSDK.addInterceptor(new CustomExpiredInterceptor());
}
/**
* 初始化XPage页面框架
*/
private static void initXPage(Application application) {
PageConfig.getInstance()
.debug(MyApp.isDebug())
.setContainActivityClazz(BaseActivity.class)
.init(application);
}
/**
* 初始化XAOP
*/
private static void initXAOP(Application application) {
XAOP.init(application);
XAOP.debug(MyApp.isDebug());
//设置动态申请权限切片 申请权限被拒绝的事件响应监听
XAOP.setOnPermissionDeniedListener(permissionsDenied -> XToastUtils.error("权限申请被拒绝:" + StringUtils.listToString(permissionsDenied, ",")));
}
/**
* 初始化XUI框架
*/
private static void initXUI(Application application) {
XUI.init(application);
XUI.debug(MyApp.isDebug());
}
/**
* 初始化路由框架
*/
private static void initRouter(Application application) {
// 这两行必须写在init之前,否则这些配置在init过程中将无效
if (MyApp.isDebug()) {
XRouter.openLog(); // 打印日志
XRouter.openDebug(); // 开启调试模式(如果在InstantRun模式下运行,必须开启调试模式!线上版本需要关闭,否则有安全风险)
}
XRouter.init(application);
}
}
View Binding是Android Studio 3.6推出的新特性,目的是为了替代findViewById(内部实现还是使用findViewById)。。在启动视图绑定后,系统会为改模块中的每个xml文件生成一个绑定类,绑定类的实例包含对在相应布局中具有 ID 的所有视图的直接引用。
通过 Viewbinding 创建 Activity 的基类
public class BaseActivity<Binding extends ViewBinding> extends XPageActivity {
/**
* 是否支持侧滑返回
*/
public static final String KEY_SUPPORT_SLIDE_BACK = "key_support_slide_back";
/**
* ViewBinding
*/
protected Binding binding;
@Override
protected void attachBaseContext(Context newBase) {
//注入字体
super.attachBaseContext(ViewPumpContextWrapper.wrap(newBase));
}
@Override
protected View getCustomRootView() {
binding = viewBindingInflate(getLayoutInflater());
return binding != null ? binding.getRoot() : null;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
initStatusBarStyle();
super.onCreate(savedInstanceState);
registerSlideBack();
}
/**
* 构建ViewBinding
*
* @param inflater inflater
* @return ViewBinding
*/
@Nullable
protected Binding viewBindingInflate(LayoutInflater inflater) {
return null;
}
/**
* 获取Binding
*
* @return Binding
*/
public Binding getBinding() {
return binding;
}
/**
* 初始化状态栏的样式
*/
protected void initStatusBarStyle() {
}
/**
* 打开fragment
*
* @param clazz 页面类
* @param addToBackStack 是否添加到栈中
* @return 打开的fragment对象
*/
public <T extends XPageFragment> T openPage(Class<T> clazz, boolean addToBackStack) {
CoreSwitchBean page = new CoreSwitchBean(clazz)
.setAddToBackStack(addToBackStack);
return (T) openPage(page);
}
/**
* 打开fragment
*
* @return 打开的fragment对象
*/
public <T extends XPageFragment> T openNewPage(Class<T> clazz) {
CoreSwitchBean page = new CoreSwitchBean(clazz)
.setNewActivity(true);
return (T) openPage(page);
}
public <T extends XPageFragment> T openNewPage(Class<T> clazz, Bundle bundle) {
CoreSwitchBean page = new CoreSwitchBean(clazz, bundle)
.setNewActivity(true);
return (T) openPage(page);
}
/**
* 切换fragment
*
* @param clazz 页面类
* @return 打开的fragment对象
*/
public <T extends XPageFragment> T switchPage(Class<T> clazz) {
return openPage(clazz, false);
}
/**
* 序列化对象
*
* @param object
* @return
*/
public String serializeObject(Object object) {
return XRouter.getInstance().navigation(SerializationService.class).object2Json(object);
}
@Override
protected void onRelease() {
unregisterSlideBack();
super.onRelease();
}
/**
* 注册侧滑回调
*/
protected void registerSlideBack() {
if (isSupportSlideBack()) {
SlideBack.with(this)
.haveScroll(true)
.edgeMode(ResUtils.isRtl() ? SlideBack.EDGE_RIGHT : SlideBack.EDGE_LEFT)
.callBack(this::popPage)
.register();
}
}
/**
* 注销侧滑回调
*/
protected void unregisterSlideBack() {
if (isSupportSlideBack()) {
SlideBack.unregister(this);
}
}
/**
* @return 是否支持侧滑返回
*/
protected boolean isSupportSlideBack() {
CoreSwitchBean page = getIntent().getParcelableExtra(CoreSwitchBean.KEY_SWITCH_BEAN);
return page == null || page.getBundle() == null || page.getBundle().getBoolean(KEY_SUPPORT_SLIDE_BACK, true);
}
}
通过 Viewbinding 创建 Fragment 的基类(使用XPage框架搭建)
public abstract class BaseFragment<Binding extends ViewBinding> extends XPageFragment {
private IProgressLoader mIProgressLoader;
/**
* ViewBinding
*/
protected Binding binding;
@Override
protected View inflateView(LayoutInflater inflater, ViewGroup container) {
binding = viewBindingInflate(inflater, container);
return binding.getRoot();
}
/**
* 构建ViewBinding
*
* @param inflater inflater
* @param container 容器
* @return ViewBinding
*/
@NonNull
protected abstract Binding viewBindingInflate(LayoutInflater inflater, ViewGroup container);
/**
* 获取Binding
*
* @return Binding
*/
public Binding getBinding() {
return binding;
}
@Override
protected void initPage() {
initTitle();
initViews();
initListeners();
}
protected TitleBar initTitle() {
return TitleUtils.addTitleBarDynamic((ViewGroup) getRootView(), getPageTitle(), v -> popToBack());
}
@Override
protected void initListeners() {
}
/**
* 获取进度条加载者
*
* @return 进度条加载者
*/
public IProgressLoader getProgressLoader() {
if (mIProgressLoader == null) {
mIProgressLoader = ProgressLoader.create(getContext());
}
return mIProgressLoader;
}
/**
* 获取进度条加载者
*
* @param message 提示信息
* @return 进度条加载者
*/
public IProgressLoader getProgressLoader(String message) {
if (mIProgressLoader == null) {
mIProgressLoader = ProgressLoader.create(getContext(), message);
} else {
mIProgressLoader.updateMessage(message);
}
return mIProgressLoader;
}
@Override
public void onConfigurationChanged(@NonNull Configuration newConfig) {
//屏幕旋转时刷新一下title
super.onConfigurationChanged(newConfig);
ViewGroup root = (ViewGroup) getRootView();
if (root.getChildAt(0) instanceof TitleBar) {
root.removeViewAt(0);
initTitle();
}
}
@Override
public void onDestroyView() {
if (mIProgressLoader != null) {
mIProgressLoader.dismissLoading();
}
super.onDestroyView();
binding = null;
}
@Override
public void onResume() {
super.onResume();
MobclickAgent.onPageStart(getPageName());
}
@Override
public void onPause() {
super.onPause();
MobclickAgent.onPageEnd(getPageName());
}
//==============================页面跳转api===================================//
/**
* 打开一个新的页面【建议只在主tab页使用】
*
* @param clazz 页面的类
* @param
* @return
*/
public <T extends XPageFragment> Fragment openNewPage(Class<T> clazz) {
return new PageOption(clazz)
.setNewActivity(true)
.open(this);
}
/**
* 打开一个新的页面【建议只在主tab页使用】
*
* @param pageName 页面名
* @param
* @return
*/
public <T extends XPageFragment> Fragment openNewPage(String pageName) {
return new PageOption(pageName)
.setAnim(CoreAnim.slide)
.setNewActivity(true)
.open(this);
}
/**
* 打开一个新的页面【建议只在主tab页使用】
*
* @param clazz 页面的类
* @param containActivityClazz 页面容器
* @param
* @return
*/
public <T extends XPageFragment> Fragment openNewPage(Class<T> clazz, @NonNull Class<? extends XPageActivity> containActivityClazz) {
return new PageOption(clazz)
.setNewActivity(true)
.setContainActivityClazz(containActivityClazz)
.open(this);
}
/**
* 打开一个新的页面【建议只在主tab页使用】
*
* @param clazz 页面的类
* @param key 入参的键
* @param value 入参的值
* @param
* @return
*/
public <T extends XPageFragment> Fragment openNewPage(Class<T> clazz, String key, Object value) {
PageOption option = new PageOption(clazz).setNewActivity(true);
return openPage(option, key, value);
}
public Fragment openPage(PageOption option, String key, Object value) {
if (value instanceof Integer) {
option.putInt(key, (Integer) value);
} else if (value instanceof Float) {
option.putFloat(key, (Float) value);
} else if (value instanceof String) {
option.putString(key, (String) value);
} else if (value instanceof Boolean) {
option.putBoolean(key, (Boolean) value);
} else if (value instanceof Long) {
option.putLong(key, (Long) value);
} else if (value instanceof Double) {
option.putDouble(key, (Double) value);
} else if (value instanceof Parcelable) {
option.putParcelable(key, (Parcelable) value);
} else if (value instanceof Serializable) {
option.putSerializable(key, (Serializable) value);
} else {
option.putString(key, serializeObject(value));
}
return option.open(this);
}
/**
* 打开页面
*
* @param clazz 页面的类
* @param addToBackStack 是否加入回退栈
* @param key 入参的键
* @param value 入参的值
* @param
* @return
*/
public <T extends XPageFragment> Fragment openPage(Class<T> clazz, boolean addToBackStack, String key, String value) {
return new PageOption(clazz)
.setAddToBackStack(addToBackStack)
.putString(key, value)
.open(this);
}
/**
* 打开页面
*
* @param clazz 页面的类
* @param key 入参的键
* @param value 入参的值
* @param
* @return
*/
public <T extends XPageFragment> Fragment openPage(Class<T> clazz, String key, Object value) {
return openPage(clazz, true, key, value);
}
/**
* 打开页面
*
* @param clazz 页面的类
* @param addToBackStack 是否加入回退栈
* @param key 入参的键
* @param value 入参的值
* @param
* @return
*/
public <T extends XPageFragment> Fragment openPage(Class<T> clazz, boolean addToBackStack, String key, Object value) {
PageOption option = new PageOption(clazz).setAddToBackStack(addToBackStack);
return openPage(option, key, value);
}
/**
* 打开页面
*
* @param clazz 页面的类
* @param key 入参的键
* @param value 入参的值
* @param
* @return
*/
public <T extends XPageFragment> Fragment openPage(Class<T> clazz, String key, String value) {
return new PageOption(clazz)
.putString(key, value)
.open(this);
}
/**
* 打开页面,需要结果返回
*
* @param clazz 页面的类
* @param key 入参的键
* @param value 入参的值
* @param requestCode 请求码
* @param
* @return
*/
public <T extends XPageFragment> Fragment openPageForResult(Class<T> clazz, String key, Object value, int requestCode) {
PageOption option = new PageOption(clazz).setRequestCode(requestCode);
return openPage(option, key, value);
}
/**
* 打开页面,需要结果返回
*
* @param clazz 页面的类
* @param key 入参的键
* @param value 入参的值
* @param requestCode 请求码
* @param
* @return
*/
public <T extends XPageFragment> Fragment openPageForResult(Class<T> clazz, String key, String value, int requestCode) {
return new PageOption(clazz)
.setRequestCode(requestCode)
.putString(key, value)
.open(this);
}
/**
* 打开页面,需要结果返回
*
* @param clazz 页面的类
* @param requestCode 请求码
* @param
* @return
*/
public <T extends XPageFragment> Fragment openPageForResult(Class<T> clazz, int requestCode) {
return new PageOption(clazz)
.setRequestCode(requestCode)
.open(this);
}
/**
* 序列化对象
*
* @param object 需要序列化的对象
* @return 序列化结果
*/
public String serializeObject(Object object) {
return XRouter.getInstance().navigation(SerializationService.class).object2Json(object);
}
/**
* 反序列化对象
*
* @param input 反序列化的内容
* @param clazz 类型
* @return 反序列化结果
*/
public <T> T deserializeObject(String input, Type clazz) {
return XRouter.getInstance().navigation(SerializationService.class).parseObject(input, clazz);
}
@Override
protected void hideCurrentPageSoftInput() {
if (getActivity() == null) {
return;
}
// 记住,要在xml的父布局加上android:focusable="true" 和 android:focusableInTouchMode="true"
Utils.hideSoftInputClearFocus(getActivity().getCurrentFocus());
}
}