Android极简MVVM,从一个基类库谈起

简介: 这篇文章,主要详细介绍如何封装一个MVVM的基类库,以及MVVM架构模式在实际业务中的用法,最后会把实际的封装代码开源,并提供远程依赖。

Hello啊各位老铁,今天带来一个老生常谈的技术,MVVM,这篇文章,主要详细介绍如何封装一个MVVM的基类库,以及MVVM架构模式在实际业务中的用法,最后会把实际的封装代码开源,并提供远程依赖,方便给到大家使用以及二次修改,尽量做到细致入微,浅显易懂,OK,废话不多赘述,我们进入正文。


这篇文章大概会按照以下几个模块进行阐述,此次封装,做到绝无第三方依赖,都是Android原生的代码封装,请放心使用,如果您想直接进行使用,请直接跳到第4步,集成使用即可,此次的封装,和目前主流的MVVM架构模式,会完美契合,让架构模式简单化,让业务代码清晰化,必须值得推荐使用。


一、MVVM简单概括

二、基于MVVM模式如何封装基类库

三、实战封装

四、封装后在业务中如何使用

五、开源以及Demo查看


一、MVVM简单概括


MVVM的开发模式,相对来说低耦合,业务之间逻辑显得也十分分明,Model层负责将请求的数据交给ViewModel层;ViewModel层负责将请求到的数据做业务逻辑处理,最后交给View层去展示,与View一一对应;View层只负责界面绘制刷新,不处理业务逻辑,非常适合进行独立模块开发。

三层简单概括

1、Model:数据层,包含数据实体和对数据实体的操作。
2、View:视图层,对应于Activity,XML,View,负责数据显示以及用户交互。
3、ViewModel:关联层,将Model和View进行绑定,Model或者View更改时,实时刷新对方。


需要注意:


1、View只做和UI相关的工作,不涉及任何业务逻辑,不涉及操作数据,不处理数据,也就是UI和数据是严格分开的。

2、ViewModel只做和业务逻辑相关的工作,不涉及任何和UI相关的操作,不持有控件引用,不更新UI。


二、基于MVVM模式如何封装基类库


MVVM我们已经清晰,然而针对现有的三层,我们如何进行拆解封装呢?面对这样的一个问题,我们也是需要从三层以及和实际的业务进行相结合,从实际业务中来,也要从实际业务中去,这是我们封装的一个潜在因素,一旦脱离了实际,封装的再优秀,也只是一个花瓶,中看不中用。


针对MVVM中的三层,其实,我们在封装中,也是基于这三层,View,ViewModel和Model。View中,在实际的开发中,一般针对Activity和Fragment进行系统的抽取封装,ViewModel一般会抽取一个父类,做一些公共的方法或属性配置,Model层一般封装的较少,根据实际业务,需要具体问题具体分析。


Activity和Fragment的封装思路,其实是一致的,需要以简单和复杂两种方向进行抽取,一种是简单的页面继承使用,一种是复杂的页面继承使用,这样区分的一个目的,就是,专职专用,避免大材小用,而具体的封装,除了使得代码简洁化,更重要的拓展化,方便子类的调用。


在具体封装的时候,与实际业务相结合,这个无比重要,比如实际的大部分页面,都带有一个标题栏,那么标题栏就可以直接封装父类里面,像子类拓展出,更改标题,右侧按钮,左侧按钮等功能属性;除了统一的标题栏,另外就是子类的视图了,关于子类的视图传递,这个是必须的,可以直接抽象出一个必须要实现的方法,其他的,比如状态栏的改变,缺省页的设置等等,也需要在父类中统一的给出。


复杂的页面是基于简单的页面而来的,这里的复杂,一般是包含很多逻辑的处理,那么,我们就可以增加ViewModel层和Model层了,目前基于DataBinding的实现方式,无论简单和复杂,都是必须需要考虑的,也就是说在父类中,我们就需要向子类提供出可以拿到的databinding和viewmodel,这样子类再继承的时候,就可以很方便的进行调用。


在复杂的页面,也就是包含ViewModel层和Model层的时候,需要考虑绑定视图variable的传递,也就是当前的ViewModel和那个xml进行绑定,当然这是在需要的时候,必须要操作的,除了视图绑定,常见的,数据请求状态,比如请求成功,请求失败,缺省页显示和隐藏,Dialog的显示和隐藏,LiveData的数据回传等等,在复杂的页面中也是需要我们考虑的。


除了以上的常规考虑,在实际的业务中,比如事件消息传递,PagerAdapter使用,状态栏透明等很多和基类的相关的功能,我们其实也可以进行封装进去,便于子类的调用。


三、实战封装


通过第2条中的拆解和具体的封装思路,不妨我们进行实战一下,由于Activity和Fragment的封装思路以及相关属性和方法,大部分都是雷同的,所以目前只介绍Activity,更详细的封装,还请大家参考源码。


1、Activity的简单封装


简单封装,不携带ViewModel,只传递ViewDataBinding,子类必须重写的方法只有一个initData,其他均为选择性重写,如果相对逻辑比较简单的页面,可以继承此类。


一个很简单的普通封装,就是把共有的常见的,封装到父类里,便于子类的调用,具体什么方法,什么逻辑进行采取封装,需要我们根据具体业务或者公司的相关情况而定,以下是源码。


abstractclassBaseActivity<VB : ViewDataBinding>(@LayoutReslayoutId: Int=0) :
AppCompatActivity(layoutId) {
privatevarmActionBarView: ActionBarView?=nullprivatevarmLayoutError: LinearLayout?=nullprivatevarmLayoutId=layoutIdlateinitvarmBinding: VBoverridefunonCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
try {
//默认状态栏为白底黑字darkMode(BaseConfig.statusBarDarkMode)
statusBarColor(ContextCompat.getColor(this, BaseConfig.statusBarColor))
setContentView(R.layout.activity_base)
valbaseChild=findViewById<LinearLayout>(R.id.layout_base_child)
mLayoutError=findViewById(R.id.layout_empty_or_error)
mActionBarView=findViewById(R.id.action_bar)
if (mLayoutId==0) {
mLayoutId=getLayoutId()
            }
if (savedInstanceState!=null&&getIntercept()) {
noEmptyBundle()
return            }
valchildView=layoutInflater.inflate(mLayoutId, null)
baseChild.addView(childView)
mBinding=DataBindingUtil.bind(childView)!!initView()
initData()
        } catch (e: Exception) {
e.printStackTrace()
noEmptyBundle()
        }
    }
/*** AUTHOR:AbnerMing* INTRODUCE:获取视图id*/openfungetLayoutId(): Int {
return0    }
openfuninitView() {}
/*** AUTHOR:AbnerMing* INTRODUCE:初始化数据*/abstractfuninitData()
/*** AUTHOR:AbnerMing* INTRODUCE:动态改变状态栏颜色和标题*/funsetDarkTitle(dark: Boolean, color: Int, title: String) {
try {
darkMode(dark)
statusBarColor(ContextCompat.getColor(this, color))
setBarTitle(title)
        } catch (e: Exception) {
e.printStackTrace()
        }
    }
/*** AUTHOR:AbnerMing* INTRODUCE:设置标题*/funsetBarTitle(title: String) {
mActionBarView!!.visibility=View.VISIBLEmActionBarView!!.setBarTitle(title)
    }
/*** AUTHOR:AbnerMing* INTRODUCE:隐藏左侧按钮*/funhintLeftMenu() {
mActionBarView!!.hintLeftBack()
    }
/*** AUTHOR:AbnerMing* INTRODUCE:获取ActionBarView*/fungetActionBarView(): ActionBarView {
returnmActionBarView!!    }
/*** AUTHOR:AbnerMing* INTRODUCE:隐藏标题栏*/funhintActionBar() {
mActionBarView?.visibility=View.GONE    }
/*** AUTHOR:AbnerMing* INTRODUCE:Bundle为空进行拦截,解决改变权限后重回App崩溃问题*/openfungetIntercept(): Boolean {
returnfalse    }
/*** AUTHOR:AbnerMing* INTRODUCE:Bundle为空时的逻辑处理,解决改变权限后重回App崩溃问题*/openfunnoEmptyBundle() {}
overridefunonDestroy() {
super.onDestroy()
try {
LiveDataBus.removeObserve(this)
LiveDataBus.removeStickyObserver(this)
        } catch (e: Exception) {
e.printStackTrace()
        }
    }
/*** AUTHOR:AbnerMing* INTRODUCE:透明状态栏*/funtranslucentWindow(dark: Boolean) {
try {
immersive(0, dark)
        } catch (e: Exception) {
e.printStackTrace()
        }
    }
/*** AUTHOR:AbnerMing* INTRODUCE:设置缺省页*/funsetEmptyOrError(view: View) {
mLayoutError?.visibility=View.VISIBLEmLayoutError?.removeAllViews()
mLayoutError?.addView(view)
    }
/*** AUTHOR:AbnerMing* INTRODUCE:隐藏*/funhintEmptyOrErrorView() {
mLayoutError?.visibility=View.GONE    }
/*** AUTHOR:AbnerMing* INTRODUCE:获取错误或为空的view*/fungetEmptyOrErrorView(): LinearLayout {
returnmLayoutError!!    }
}


涉及的方法概述


方法名

参数

概述

getLayoutId

无参

子类传递的layout,用于加载视图,可以通过构造方法传递,也可以通过此方法传递。

initView

无参

初始化View,非必须重写

initData

无参

初始化数据

setDarkTitle

dark: Boolean, color: Int, title: String

1、dark: Boolean,状态栏颜色,true就是黑色,false就是白色。

2、color: Int,状态栏背景颜色

3、title: String,标题栏内容

设置标题,状态栏背景及颜色

setBarTitle

title: String,标题栏内容

设置标题

hintLeftMenu

无参

隐藏左侧按钮

getActionBarView

无参

获取标题栏View,可以操作标题栏里的任何控件

hintActionBar

无参

隐藏标题栏

translucentWindow

dark: Boolean,状态栏颜色,true就是黑色,false就是白色

透明状态栏

setEmptyOrError

view: View,传递的缺省View视图

设置缺省视图

hintEmptyOrErrorView

无参

隐藏缺省视图

getEmptyOrErrorView

无参

获取缺省视图


简单的Activity没有什么好说的,都是中规中矩,具体的使用请大家看第四条,具体使用即可。


2、Activity的复杂封装


也谈不上复杂,只是在继承简单页面的基础之上多加了一个ViewModel,相对于比较复杂的页面,就可以继承此类,此类,拓展了ViewModel,可以在ViewModel里进行逻辑的书写,此类也是MVVM的标准执行,V继承于BaseVMActivity,VM继承于BaseViewModel,至于M,可以在VM中通过getRepository方法进行获取。


具体代码逻辑如下:


BaseVMActivity继承于BaseActivity。


abstractclassBaseVMActivity<VB : ViewDataBinding, BM : BaseViewModel>(@LayoutReslayoutId: Int=0) :
BaseActivity<VB>(layoutId) {
lateinitvarmViewModel: BMoverridefuninitData() {
mViewModel=getViewModel()!!valvariableId=getVariableId()
if (variableId!=-1) {
mBinding.setVariable(getVariableId(), mViewModel)
mBinding.executePendingBindings()
        }
initVMData()
observeLiveData()
initState()
    }
/*** AUTHOR:AbnerMing* INTRODUCE:获取绑定的xml id*/openfungetVariableId(): Int {
return-1    }
/*** AUTHOR:AbnerMing* INTRODUCE:初始化状态*/privatefuninitState() {
mViewModel.mStateViewLiveData.observe(this, {
when (it) {
StateLayoutEnum.DIALOG_LOADING-> {
dialogLoading()
                }
StateLayoutEnum.DIALOGD_DISMISS-> {
dialogDismiss()
                }
StateLayoutEnum.DATA_ERROR-> {
dataError()
                }
StateLayoutEnum.DATA_NULL-> {
dataEmpty()
                }
StateLayoutEnum.NET_ERROR-> {
netError()
                }
StateLayoutEnum.HIDE-> {
hide()
                }
            }
        })
    }
/*** AUTHOR:AbnerMing* INTRODUCE:初始化数据*/abstractfuninitVMData()
/*** AUTHOR:AbnerMing* INTRODUCE:LiveData的Observer*/openfunobserveLiveData() {
    }
/*** AUTHOR:AbnerMing* INTRODUCE:dialog加载*/openfundialogLoading() {}
/*** AUTHOR:AbnerMing* INTRODUCE:dialog隐藏*/openfundialogDismiss() {}
/*** AUTHOR:AbnerMing* INTRODUCE:数据错误*/openfundataError() {}
/*** AUTHOR:AbnerMing* INTRODUCE:数据为空*/openfundataEmpty() {}
/*** AUTHOR:AbnerMing* INTRODUCE:网络错误或请求错误*/openfunnetError() {}
/*** AUTHOR:AbnerMing* INTRODUCE:隐藏某些布局或者缺省页等*/openfunhide() {}
privatefungetViewModel(): BM? {
//这里获得到的是类的泛型的类型valtype=javaClass.genericSuperclassif (type!=null&&typeisParameterizedType) {
valactualTypeArguments=type.actualTypeArgumentsvaltClass=actualTypeArguments[1]
returnViewModelProvider(
this,
ViewModelProvider.AndroidViewModelFactory.getInstance(application)
            )
                .get(tClassasClass<BM>)
        }
returnnull    }
}


封装涉及的方法概述


方法名

参数

概述

getVariableId

无参

获取绑定的xml variable,也就是当前的xml和哪个对象进行绑定,用于xml里直接数据绑定

initVMData

无参

初始化数据,必须要实现的方法

observeLiveData

无参

LiveData的Observer,UI层监听ViewModel层的数据改变

dialogLoading

无参

dialog加载

dialogDismiss

无参

dialog隐藏

dataError

无参

数据错误

dataEmpty

无参

数据为空

netError

无参

数据错误

hide

无参

隐藏缺省页等其他页面


BaseViewModel


BaseViewModel相对比较简单,只提供了一个可以获取Repository的方法,还有一个是刷新UI视图的一个LiveData,就是数据请求,Dialog加载,缺省页加载的状态。


openclassBaseViewModel : ViewModel() {
/*** 控制状态视图的LiveData*/valmStateViewLiveData=MutableLiveData<StateLayoutEnum>()
/*** 更改状态视图的状态*/publicfunchangeStateView(
state: StateLayoutEnum    ) {
// 对参数进行校验when (state) {
StateLayoutEnum.DIALOG_LOADING-> {
mStateViewLiveData.postValue(StateLayoutEnum.DIALOG_LOADING)
            }
StateLayoutEnum.DIALOGD_DISMISS-> {
mStateViewLiveData.postValue(StateLayoutEnum.DIALOGD_DISMISS)
            }
StateLayoutEnum.DATA_ERROR-> {
mStateViewLiveData.postValue(StateLayoutEnum.DATA_ERROR)
            }
StateLayoutEnum.DATA_NULL-> {
mStateViewLiveData.postValue(StateLayoutEnum.DATA_NULL)
            }
StateLayoutEnum.NET_ERROR-> {
mStateViewLiveData.postValue(StateLayoutEnum.NET_ERROR)
            }
StateLayoutEnum.HIDE-> {
mStateViewLiveData.postValue(StateLayoutEnum.HIDE)
            }
        }
    }
/*** AUTHOR:AbnerMing* INTRODUCE:获取Repository*/inlinefun<reifiedR>getRepository(): R? {
try {
valclazz=R::class.javareturnclazz.newInstance()
        } catch (e: Exception) {
e.printStackTrace()
        }
returnnull    }
}


复杂的Activity,大家可以发现,其实就是标准的MVVM形式封装,Fragment的封装也是基于此,搞清楚上述,基本上我们这个基类库就完成了大半,确实也没什么好说的,大家直接看使用吧。


四、封装后在业务中如何使用


通过以上的封装,我们在业务层所有的页面就可以继承父类进行使用,以达到代码的高度统一,使得架构模式简单化,让业务代码清晰化,目前的封装,大家可以直接封装成库或者打成aar给到其他开发者使用,目前我已经上传到远程,不想麻烦的老铁,可以直接按照下面的步骤进行使用。


1、在你的根项目下的build.gradle文件下,引入maven


allprojects {
repositories {
maven { url"https://gitee.com/AbnerAndroid/almighty/raw/master" }
    }
}


2、在你需要使用的Module中build.gradle文件下,引入依赖。


dependencies {
implementation'com.vip:base:1.0.1'}


通过以上的Maven仓库依赖,我们就可以愉快的进行使用了,下面针对各个封装的功能进行一个简单的演示,当然,大家可以直接看源码中的实例,那里相对比较全面。


1、普通的Activity的继承


如果,你的Activity页面逻辑比较简单,建议继承BaseActivity,此父类,没有与ViewModel相结合,只包含正常且简单的逻辑处理,目前必须重写的只有一个initData方法,其他方法,大家可以根据业务重写即可。


classMainActivity : BaseActivity<ActivityMainBinding>(R.layout.activity_main) {
/*** AUTHOR:AbnerMing* INTRODUCE:初始化数据*/overridefuninitData() {
setBarTitle("主页")
    }
}

2、ViewModel形式Activity的继承

View层,需要继承BaseVMActivity

classTestViewModelActivity : BaseVMActivity<ActivityViewModelBinding,
TestViewModel>(R.layout.activity_view_model) {
overridefuninitVMData() {
setBarTitle("ViewModel方式使用")
    }
}

ViewModel层,需要继承BaseViewModel

classTestViewModel : BaseViewModel() {
/*** AUTHOR:AbnerMing* INTRODUCE:获取需要的Repository*/privatevalrepositorybylazy {
getRepository<TestRepository>()
    }
}

Model层,一般根据实际需要,进行具体的封装使用。

classTestRepository {
}


3、DataBinding形式使用

View层,继承BaseVMActivity,返回当前视图的绑定variable


classDataBindActivity :
BaseVMActivity<ActivityDataBindBinding,
DataBindViewModel>(R.layout.activity_data_bind) {
overridefuninitVMData() {
setBarTitle("DataBinding使用")
    }
overridefungetVariableId(): Int {
returnBR.data    }
}


ViewModel层继承BaseViewModel


classDataBindViewModel : BaseViewModel() {
varoneWayContent="单向绑定数据测试"vartwoWayContent="双向绑定数据测试"/*** AUTHOR:AbnerMing* INTRODUCE:获取双向绑定数据*/varclickListener=View.OnClickListener {
Toast.makeText(it.context, twoWayContent, Toast.LENGTH_SHORT).show()
    }
}


XML视图,直接绑定


<layoutxmlns:android="http://schemas.android.com/apk/res/android"xmlns:app="http://schemas.android.com/apk/res-auto"><data><variablename="data"type="com.abner.base.bind.DataBindViewModel"/></data><LinearLayoutandroid:layout_width="match_parent"android:layout_height="match_parent"android:orientation="vertical"android:paddingLeft="@dimen/gwm_dp_20"android:paddingRight="@dimen/gwm_dp_20"><TextViewandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_gravity="center"android:layout_marginTop="@dimen/gwm_dp_20"android:text="@{data.oneWayContent}"/><EditTextandroid:layout_width="match_parent"android:layout_height="wrap_content"android:layout_marginTop="@dimen/gwm_dp_20"android:hint="双向绑定"android:text="@={data.twoWayContent}"/><Buttonandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_gravity="center"android:layout_marginTop="@dimen/gwm_dp_20"android:onClick="@{data.clickListener}"android:text="获取双向绑定数据"/></LinearLayout></layout>


4、Fragment的简单使用


如果,你的Fragment页面逻辑比较简单,建议继承BaseFragment,此父类,没有与ViewModel相结合,只包含正常且简单的逻辑处理,目前必须重写的只有一个initData方法,其他方法,大家可以根据业务重写即可。


classTestPagerFragment : BaseFragment<FragmentTestPagerBinding>(R.layout.fragment_test_pager) {
overridefuninitData() {
    }
}


5、ViewModel形式Fragment的继承


View层,需要继承BaseVMFragment


classTestViewModelPagerFragment :
BaseVMFragment<FragmentTestPagerBinding,
TestFragmentViewModel>(R.layout.fragment_test_pager) {
overridefuninitVMData() {
    }
}


ViewModel层,需要继承BaseViewModel


classTestFragmentViewModel :BaseViewModel(){
/*** AUTHOR:AbnerMing* INTRODUCE:获取需要的Repository*/privatevalrepositorybylazy {
getRepository<TestRepository>()
    }
}


Model层,一般根据实际需要,进行具体的封装使用。


classTestRepository {
}


6、Fragment的DataBinding形式使用和Activity类似,就不赘述了。

7、事件消息总线使用

普通事件发送

LiveDataBus.send("send", "我发送了一条普通消息")

普通发送事件接收

LiveDataBus.observe(this, "send", Observer<String> {
Toast.makeText(this, it, Toast.LENGTH_SHORT).show()
        })

   

粘性事件发送


LiveDataBus.sendSticky("sendSticky", "我发送了一条粘性事件消息")

 

粘性事件接收


LiveDataBus.observeSticky(this, "sendSticky", Observer<String> {
Toast.makeText(this, it, Toast.LENGTH_SHORT).show()
        })

 

更多的其他功能使用,大家直接看Github即可,上边有比较清晰的介绍。



五、开源以及Demo查看


以上的封装,目前已经开源,大家可以下载查看源码,或者进行二次更改使用,地址是:

https://github.com/AbnerMing888/VipBase


相关Demo,大家可以down下项目,运行即可,这里简单贴张效果图:


目前的封装,没有过多的冗余代码,完全可以满足实际的业务需要,大家可以按照这种模式试验一番,遇到问题,可以多多交流,毕竟,技术是开放的,交流中才能不断的进步,当然了,需要结合自己的实际业务进行使用,毕竟项目中不应存在多个架构模式,MVC也好,MVPMVVMMVI也罢,无论使用哪种,适合的才是最好的。


好了,各位老铁,这篇文章就到这里,下篇文章《组件化开发,从未如此简单》正在撰写中,大家敬请期待!

相关文章
|
1月前
|
前端开发 JavaScript 测试技术
android做中大型项目完美的架构模式是什么?是MVVM吗?如果不是,是什么?
android做中大型项目完美的架构模式是什么?是MVVM吗?如果不是,是什么?
97 2
|
1月前
|
存储 前端开发 Java
Android MVVM架构模式下如何避免内存泄漏
Android采用MVVM架构开发项目,如何避免内存泄漏风险?怎样避免内存泄漏?
88 1
|
13天前
|
前端开发 JavaScript 测试技术
android做中大型项目完美的架构模式是什么?是MVVM吗?如果不是,是什么?
在 Android 开发中,选择合适的架构模式对于构建中大型项目至关重要。常见的架构模式有 MVVM、MVP、MVI、Clean Architecture 和 Flux/Redux。每种模式都有其优缺点和适用场景,例如 MVVM 适用于复杂 UI 状态和频繁更新,而 Clean Architecture 适合大型项目和多平台开发。选择合适的架构应考虑项目需求、团队熟悉度和可维护性。
40 6
|
23天前
|
存储 前端开发 测试技术
Android kotlin MVVM 架构简单示例入门
Android kotlin MVVM 架构简单示例入门
28 1
|
25天前
|
安全 Java 网络安全
Android远程连接和登录FTPS服务代码(commons.net库)
Android远程连接和登录FTPS服务代码(commons.net库)
22 1
|
29天前
|
Linux API 开发工具
FFmpeg开发笔记(五十九)Linux编译ijkplayer的Android平台so库
ijkplayer是由B站研发的移动端播放器,基于FFmpeg 3.4,支持Android和iOS。其源码托管于GitHub,截至2024年9月15日,获得了3.24万星标和0.81万分支,尽管已停止更新6年。本文档介绍了如何在Linux环境下编译ijkplayer的so库,以便在较新的开发环境中使用。首先需安装编译工具并调整/tmp分区大小,接着下载并安装Android SDK和NDK,最后下载ijkplayer源码并编译。详细步骤包括环境准备、工具安装及库编译等。更多FFmpeg开发知识可参考相关书籍。
81 0
FFmpeg开发笔记(五十九)Linux编译ijkplayer的Android平台so库
|
13天前
|
前端开发 Java 测试技术
android MVP契约类架构模式与MVVM架构模式,哪种架构模式更好?
android MVP契约类架构模式与MVVM架构模式,哪种架构模式更好?
22 0
|
1月前
|
前端开发 Java 测试技术
android MVP契约类架构模式与MVVM架构模式,哪种架构模式更好?
android MVP契约类架构模式与MVVM架构模式,哪种架构模式更好?
20 2
|
1月前
|
Ubuntu Shell API
Ubuntu 64系统编译android arm64-v8a 的openssl静态库libssl.a和libcrypto.a
Ubuntu 64系统编译android arm64-v8a 的openssl静态库libssl.a和libcrypto.a
|
3月前
|
编解码 测试技术 Android开发
Android经典实战之用 CameraX 库实现高质量的照片和视频拍摄功能
本文详细介绍了如何利用CameraX库实现高质量的照片及视频拍摄功能,包括添加依赖、初始化、权限请求、配置预览与捕获等关键步骤。此外,还特别针对不同分辨率和帧率的视频拍摄提供了性能优化策略,确保应用既高效又稳定。
307 1
Android经典实战之用 CameraX 库实现高质量的照片和视频拍摄功能