android model封装,Android项目基类封装ViewBinding、MVP、ViewModel-程序员宅基地

技术标签: android model封装  

都会需要有用到BaseActivity,从最开始的initData、initView,到后来需要承载监听推送、监听网络变化的广播、延迟锁定等等各种需求,BaseActivity的功能越来越杂,越来越密集。相对实际的页面上的功能需求,基类的封装经过这样长时间的无脑堆砌,到最后看起来会更匪夷所思。所以从一开始,Base的封装就要足够清晰、稳健、可扩展。

AndroidBase

我的思路是分层继承,每一层只做和这一层功能相关的事,变动修改单一功能都是清晰的,同时在最后的一层功能又是完整的。

AppCompatActivity

|

BaseViewActivity

|

BaseFunctionsActivity

|

BaseViewModelActivity

|

BaseMVPActivity

|

BaseToolbarActivity

1.BaseViewActivity(View层)

主要代码:

public abstract class BaseViewActivity extends AppCompatActivity

{

private View rootView;

protected VB vBinding;

@Override

protected void onCreate(Bundle savedInstanceState)

{

super.onCreate(savedInstanceState);

setRequestedOrientation(getScreenOrientation());//竖屏

vBinding = ViewBindingCreator.createViewBinding(getClass(), getLayoutInflater());

rootView = generateContentView(vBinding == null ? getContentView() : vBinding.getRoot());

setContentView(rootView);

}

protected View getContentView()

{

return null;

}

protected View generateContentView(View contentView)

{

return contentView;

}

protected int getScreenOrientation()

{

return ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;

}

public void showLoadingDialog()

{

}

public void hideLoadingDialog()

{

}

}

ViewBindingCreator主要代码:

@SuppressWarnings("unchecked")

public static VB createViewBinding(Class targetClass,

LayoutInflater layoutInflater)

{

Type type = targetClass.getGenericSuperclass();

if (type instanceof ParameterizedType)

{

try

{

Type[] types = ((ParameterizedType) type).getActualTypeArguments();

for (Type type1 : types)

{

if (type1.getTypeName()

.endsWith("Binding"))

{

Method method = ((Class) type1).getMethod("inflate",

LayoutInflater.class);

return (VB) method.invoke(null, layoutInflater);

}

}

}

catch (Exception e)

{

e.printStackTrace();

}

} return null;

}

public abstract class BaseViewFragment extends Fragment

{

protected VB vBinding;

@Nullable

@Override

public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,

@Nullable Bundle savedInstanceState)

{

if (vBinding == null)

{

vBinding = ViewBindingCreator.createViewBinding(getClass(), inflater);

View rootView = generateContentView(

vBinding == null ? getContentView() : vBinding.getRoot());

rootView.setBackgroundColor(getResources().getColor(R.color.colorBackgroundTint));

onCreateView(rootView);

}

return vBinding.getRoot();

}

protected abstract void onCreateView(View rootView);

protected View getContentView()

{

return null;

}

protected View generateContentView(View contentView)

{

return contentView;

}

protected void setStatusBarTextDark(boolean isStatusBarTextDark)

{

Activity activity = getActivity();

if (activity instanceof BaseViewActivity)

{

((BaseViewActivity) activity).setStatusBarTextDark(isStatusBarTextDark);

}

}

protected void showLoadingDialog()

{

Activity activity = getActivity();

if (activity instanceof BaseViewActivity)

{

((BaseViewActivity) activity).showLoadingDialog();

}

}

protected void hideLoadingDialog()

{

Activity activity = getActivity();

if (activity instanceof BaseViewActivity)

{

((BaseViewActivity) activity).hideLoadingDialog();

}

}

}

继承自AppCompatActivity不用说什么了。用ViewModel也是因为之前用ButterKnife,更新了Android Studio4.0之后提示R.id.xxxxx不再是静态常量,所以推荐使用ViewBinding的方式,ButterKnife官方也说了不再维护,转用ViewBinding。相对ButterKnife就是少了那个对输入框 点击事件的注解方式写法。我在另一个文章吐槽过。也还行吧,官方都推荐了 ,应该不会有太大问题。BaseViewActivity内主要通过ViewBinding或者getContentView获取页面内容View,优先看有没有ViewBinding,没有的话再用getContentView。ViewBinding这块用了反射去调inflate方法来创建对象。反射是不好,但是为了可以偷懒避免在每个具体xxxActivity里都写一遍xxxViewBinding.inflate,只需要在泛型声明一下xxxViewBinding就可以了。还提供了一个generateContentView方法,便于子类重载后扩充contentView,例如BaseToolbarActivity就是实现了这个方法,对contentView进一步包装后再返回。

在这个类内也可以封装loadingDialog,Toast什么有关View的基础功能。

同时在写具体业务的Activity或者Fragment时,不需要重载什么新的方法,只需要传一个泛型ViewBinding就可以了,减少写代码的负担。

2.BaseFunctionsActivity(基础功能层)

主要代码:

public abstract class BaseFunctionsActivity extends BaseViewActivity

{

public static ArrayList> functionsClasses = new ArrayList<>();

private LinkedHashMap functions = new LinkedHashMap<>();

@Override

protected void onCreate(Bundle savedInstanceState)

{

super.onCreate(savedInstanceState);

for (Class extends BaseActivityFunction> functionClass : functionsClasses)

{

try

{

functions.put(functionClass.getName(), functionClass.newInstance());

}

catch (IllegalAccessException | InstantiationException e)

{

e.printStackTrace();

}

}

for (BaseActivityFunction baseActivityFunction : functions.values())

{

baseActivityFunction.onActivityCreated(this, savedInstanceState);

}

}

@Override

protected void onNewIntent(Intent intent)

{

super.onNewIntent(intent);

}

@Override

protected void onSaveInstanceState(@NonNull Bundle outState)

{

super.onSaveInstanceState(outState);

for (BaseActivityFunction baseActivityFunction : functions.values())

{

baseActivityFunction.onActivitySaveInstanceState(this, outState);

}

}

@Override

protected void onStart()

{

super.onStart();

for (BaseActivityFunction baseActivityFunction : functions.values())

{

baseActivityFunction.onActivityStarted(this);

}

}

@Override

protected void onRestart()

{

super.onRestart();

for (BaseActivityFunction baseActivityFunction : functions.values())

{

baseActivityFunction.onActivityRestarted(this);

}

}

@Override

protected void onResume()

{

super.onResume();

for (BaseActivityFunction baseActivityFunction : functions.values())

{

baseActivityFunction.onActivityResumed(this);

}

}

@Override

protected void onPause()

{

super.onPause();

for (BaseActivityFunction baseActivityFunction : functions.values())

{

baseActivityFunction.onActivityPaused(this);

}

}

@Override

protected void onStop()

{

super.onStop();

for (BaseActivityFunction baseActivityFunction : functions.values())

{

baseActivityFunction.onActivityStopped(this);

}

}

@Override

protected void onDestroy()

{

super.onDestroy();

Iterator iterator = functions.values()

.iterator();

while (iterator.hasNext())

{

BaseActivityFunction baseActivityFunction = iterator.next();

baseActivityFunction.onActivityDestroyed(this);

baseActivityFunction = null;

iterator.remove();

}

functions = null;

}

@Override

public void onBackPressed()

{

boolean canBackPressed = true;

for (BaseActivityFunction value : functions.values())

{

if (!value.onBeforeBackPressed(this))

{

canBackPressed = false;

}

}

if (canBackPressed)

{

super.onBackPressed();

}

}

@Override

public void finish()

{

super.finish();

for (BaseActivityFunction value : functions.values())

{

value.onFinish(this);

}

}

public BaseActivityFunction getFunction(Class extends BaseActivityFunction> fClass)

{

return functions.get(fClass.getName());

}

}

public abstract class BaseActivityFunction implements Application.ActivityLifecycleCallbacks

{

@Override

public void onActivityCreated(Activity activity, Bundle savedInstanceState)

{

}

@Override

public void onActivityStarted(Activity activity)

{

}

public void onActivityRestarted(Activity activity)

{

}

@Override

public void onActivityResumed(Activity activity)

{

}

@Override

public void onActivityPaused(Activity activity)

{

}

@Override

public void onActivityStopped(Activity activity)

{

}

@Override

public void onActivitySaveInstanceState(Activity activity, Bundle outState)

{

}

@Override

public void onActivityDestroyed(Activity activity)

{

}

public void onFinish(Activity activity)

{

}

public boolean onBeforeBackPressed(Activity activity)

{

return true;

}

}

因为在很多场景,都需要基类Activity具有一些功能,检查是否登录,监听网络变化,嵌入友盟推送等等。本基类封装库又是一个完整的个体,所以就会出现尴尬的情况,基类是A->B->C->D->XXXXX这样的继承关系,但是我又需要在B和C之间插入一个继承B1,所以基于此需求,BaseFunctionsActivity通过实现类似ActivityCallback的方式,提供给外部嵌入内部的机会,通过继承BaseActivityFunction来实现不同功能模块。

比如我想让整个应用的所有Activity能监听网络变化,并Toast提示到界面上,我就只需要创建一个NetChangeActivityFunction,实现监听网络变化的广播,把NetChangeActivityFunction.class添加到BaseFunctionsActivity的静态functionsClasses中,这样每个Activity就具有了NetChangeActivityFunction的功能。如果某一个具体Activity想调用NetChangeActivityFunction中的方法时,用getFunction方法可以获取到当前Activity里已经实现的BaseActivityFunction具体对象,就可以调用到方法,变量同理。

同时也提供了对onBackPressed的拦截,因为有很多场景,需要在onBackPressed时判断一些逻辑,再决定是否继续执行。这块的重点设计的逻辑是,如果一个Activity有多个Function,只要其中有一个Function判定为false就不会执行super.onBackPressed();但是每一个Function的onBackPressed都会执行一次。暂时考虑对每个Function都公平一点,避免出现到某一个Function的onBackPressed就终止了后面的执行。

这样的在一个Activity里注入多个Function的方式有一个缺点就是每一个Activity都承载了多个的Function对象,如果BaseFunctions很多的话,相对直接继承多层Activity的方式,会占用更多内存。

3.BaseViewModelActivity(ViewModel层)

主要代码:

public abstract class BaseViewModelActivity extends BaseFunctionsActivity

{

protected final void setViewModelHolder(@NonNull IViewModelHolder iViewModelHolder)

{

iViewModelHolder.setIViewModelOwners(new IViewModelOwners()

{

@Override

public ViewModelStoreOwner getActivityViewModelStoreOwner()

{

return BaseViewModelActivity.this;

}

@Override

public ViewModelStoreOwner getFragmentViewModelStoreOwner()

{

return null;

}

@Override

public LifecycleOwner getActivityLifecycleOwner()

{

return BaseViewModelActivity.this;

}

@Override

public LifecycleOwner getFragmentLifecycleOwner()

{

return null;

}

});

}

}

public interface IViewModelHolder

{

void setIViewModelOwners(IViewModelOwners iViewModel);

void onViewModelLoaded();

}

public interface IViewModelOwners

{

ViewModelStoreOwner getActivityViewModelStoreOwner();

ViewModelStoreOwner getFragmentViewModelStoreOwner();

LifecycleOwner getActivityLifecycleOwner();

LifecycleOwner getFragmentLifecycleOwner();

}

public class BaseViewModelHolder implements IViewModelHolder

{

private IViewModelOwners iViewModelOwners;

private ViewModelProvider activityViewModelProvider;

private ViewModelProvider fragmentViewModelProvider;

@Override

public final void setIViewModelOwners(IViewModelOwners iViewModel)

{

this.iViewModelOwners = iViewModel;

onViewModelLoaded();

}

@Override

public void onViewModelLoaded()

{

}

protected final VM getActivityViewModel(Class vmClass)

{

if (activityViewModelProvider == null)

{

activityViewModelProvider = new ViewModelProvider(

iViewModelOwners.getActivityViewModelStoreOwner(),

new ViewModelProvider.NewInstanceFactory());

}

return activityViewModelProvider.get(vmClass);

}

protected final VM getFragmentViewModel(Class vmClass)

{

if (fragmentViewModelProvider == null)

{

fragmentViewModelProvider = new ViewModelProvider(

iViewModelOwners.getFragmentViewModelStoreOwner(),

new ViewModelProvider.NewInstanceFactory());

}

return fragmentViewModelProvider.get(vmClass);

}

protected final void observeActivityLiveData(LiveData liveData, Observer observer)

{

liveData.observe(iViewModelOwners.getActivityLifecycleOwner(), observer);

}

protected final void observeFragmentLiveData(LiveData liveData, Observer observer)

{

liveData.observe(iViewModelOwners.getFragmentLifecycleOwner(), observer);

}

}

ViewModel这块也是尝试了很多种方式,目前保持一个还算合适的状态,但是总感觉也不是最好的状态。因为对ViewModel和LiveData的学习也是近期才开始的,没有太多的应用场景可能理解的不是完全到位。

ViewModel层整体的思路是,因为ViewModel需要ViewModelStore才能创建,LiveData又需要LifeCycle,这两个需要都集中在ComponentActivity和Fragment这两个宿主中,但是ViewModel的创建又不能直接持有宿主的引用,ViewModel的使用场景绝大部分又应该都是在Presenter的内部。在一个宿主中可能存在多个ViewModel,ViewModel就要发源于宿主,在Presenter中被引用和使用。还有个限制是不希望ViewModel层高于MVP层,因为希望MVP层是具体使用场景中比较主要的,整体的架构还是围绕MVP的,ViewModel相关的只是附加提供的功能。

因为Activity和Fragment都具有ViewModelStoreOwner和LifeCycleOwner的属性,所以把他们两个类里总计四个属性:ActivityViewModelStoreOwner、ActivityLifeCycleOwner、FragmentViewModelStoreOwner和FragmentLifeCycleOwner抽象为一个新的对象IViewModelOwners(这个名字不太妥,之后得改)。这样的新对象就是脱离了Activity和Fragment的限制的,是超越了他们的。为什么一定要拆分成四个方法而不是两个,是因为有的时候需要在Fragment里用到Fragment所附着的Activity的ViewModel,所以只能拆分开。

在BaseViewModelHolder中,通过IViewModelOwners接口在Activity和Fragment中获取到ViewModelStoreOwner和LifeCycleOwner,可以创建出ViewModelProvider,再通过对应的activityViewModelProvider或者fragmentViewModelProvider创建出需要的ViewModel。LiveData的观测也是发在BaseViewModelHolder中。之后让BasePresenter继承自BaseViewModelHolder就可以获得到对应的创建ViewModel和观测LiveData功能了。

BaseMVPActivity(MVP层)

主要代码:

public abstract class BaseMVPActivity extends BaseViewModelActivity implements BaseContract.View

{

protected Presenter presenter;

@Override

protected void onCreate(Bundle savedInstanceState)

{

super.onCreate(savedInstanceState);

presenter = createPresenter();

if (presenter != null)

{

setViewModelHolder(presenter);

}

}

@Override

public void showLoading()

{

showLoadingDialog();

}

@Override

public void hideLoading()

{

hideLoadingDialog();

}

@Override

public void message(String message)

{

ToastUtil.toast(message);

}

protected Presenter createPresenter()

{

return null;

}

@Override

protected void onDestroy()

{

super.onDestroy();

if (presenter != null)

{

presenter.detachView();

}

}

}

public interface BaseContract

{

interface View

{

void showLoading();

void hideLoading();

void message(String message);

}

interface Presenter extends IViewModelHolder

{

boolean viewNotNull();

void detachView();

}

}

public abstract class BasePresenter extends BaseViewModelHolder implements BaseContract.Presenter

{

protected View view;

protected BasePresenter(View view)

{

this.view = view;

}

@Override

public boolean viewNotNull()

{

return this.view != null;

}

@Override

public void detachView()

{

this.view = null;

}

protected VM getViewModel(Class vmClass)

{

if (view instanceof Fragment)

{

return getFragmentViewModel(vmClass);

}

return getActivityViewModel(vmClass);

}

protected void observeLiveData(LiveData liveData, Observer observer)

{

if (view instanceof Fragment)

{

observeFragmentLiveData(liveData, getMVPObserver(observer));

}

else

{

observeActivityLiveData(liveData, getMVPObserver(observer));

}

}

private Observer getMVPObserver(Observer observer)

{

return t -> {

if (viewNotNull())

{

observer.onChanged(t);

}

};

}

}

MVP的封装还是比较基础的,BaseContract内含View和Presenter的接口,Activity持有Presenter的引用,Presenter持有View的引用,同时BasePresenter继承了BaseViewModelHolder,也就有了M层的功能,但是又相对BaseContract的这一套是独立的,ViewModel相关并不与MVP强相关。同时对获取ViewModel做了进一步封装,减少BasePresenter子类使用ViewModel时的判断逻辑。BaseMVPActivity还是和BaseViewActivity一样,在继承没有强制需要实现的方法,增加了Presenter的泛型,自主去实现createPresenter方法,实现了之后自然就需要让Activity实现XXXContract.View的接口。

举例创建一个Activity:通过在文件夹右键单击 New->Activity->Empty Activity创建一个TestActivity,自动创建好后只需要把继承的AppCompatActivity修改为BaseMVPActivity(也可以根据需要继承自BaseToolbarActivity)。这时都不需要实现任何抽象方法,只需要在BaseMVPActivity上配置泛型就可以,但是Presenter的创建还是要主动实现一个父类的createPresenter()方法。

这样写起代码来都是顺畅的,比如要新建一个页面,就是先新建一个XXXContract类,想好这个页面可能会有那些功能和回调,去定义View和Presenter的方法,然后新建一个XXXPresenter类实现XXXContract.Presenter接口,可以暂时不写具体功能,然后再创建一个XXXActivity,把XXXViewBinding和XXXPresenter泛型配置好。这时候可以先去写XXXViewBinding的布局页面,也可以先写XXXPresenter中的业务逻辑。脑袋里想好要改页面的东西,那就去关注xml布局,想改业务逻辑就去XXXModel或者XXXPresenter,相互之间都不干扰。git提交时也可以一眼看出来这次提交是修改了业务还是布局,减少出错。

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/weixin_30963127/article/details/117347486

智能推荐

python下载过程中最后一步执行opencv出错怎么回事_PyCharm安装opencv-python和opencv-contrib-python的一些问题和解决方法_2018-09-27...-程序员宅基地

文章浏览阅读1.9k次。当我想要在python上测试FeatureDetector并使用OpenCV的SIFT时,由于我在pycharm上仅仅安装了opencv-python,所以会出现报错(忘记截图了,好像是:‘module’ object has no attribute ‘xfeatures2d’。大致意思是说找不到 xfeatures2d 的库)。2018.9.30更新:Windows环境下把opencv中pyt..._为什么我下的python最后一步测试的时候

springcloud_originaluri.gethost() 为 null-程序员宅基地

文章浏览阅读637次。认识微服务单体架构单体架构:将业务的所有功能集中在一个项目中开发,打成一个包部署。优点:架构简单,部署成本低缺点:耦合度高(维护困难、升级困难)分布式架构分布式架构:根据业务功能对系统做拆分,每个业务功能模块作为独立项目开发,称为一个服务。优点:降低服务耦合,有利于服务升级和拓展缺点:服务调用关系错综复杂分布式架构虽然降低了服务耦合,但是服务拆分时也有很多问题需要思考:服务拆分的粒度如何界定? 服务之间如何调用? 服务的调用关系如何管理?人们需要制定.._originaluri.gethost() 为 null

狂神说JavaWeb详细笔记_狂神说javaweb笔记-程序员宅基地

文章浏览阅读2.8k次,点赞5次,收藏44次。目录概念前言WEB应用程序动态WEB的访问过程WEB服务器技术讲解Tomcat安装tomcat配置发布一个WEB网站HTTP什么是HTTPHTTP的两个时代HTTP请求HTTP请求MavenMaven项目架构管理工具在IDEA里面使用Maven创建一个普通的Maven项目标记文件夹功能解决遇到的问题Servlet简介HelloServletMaven环境优化Servlet原理概念前言静态Web:. 提供给所有人看数据不会发生变化!. HTML,CSS动态Web:.有数据交互,登录账号密码,网站_狂神说javaweb笔记

windows的域和域林间的信任是如何工作的_windows服务器跨域x信任-程序员宅基地

文章浏览阅读2k次。文章目录信任关系架构NTLM协议 (Msv1_0.dll)Kerberos协议 (Kerberos.dll)Net Logon (Netlogon.dll)LSA (Lsasrv.dll)Tools活动目录通过域和域林间的信任关系为不同的域个域林的通信提供安全保障。在通过信任关系进行认证之前,Windows必须首先确定接受请求的域是否和发起请求的账户所在的域之间拥有信任关系。要想进行这个判断,W..._windows服务器跨域x信任

网卡驱动设备watchdog-dev_watchdog_watchdog ixgbe transmit queue54 timed out-程序员宅基地

文章浏览阅读1.8k次。文章目录初始化开启watchdog超时处理dev_watchdog发送时间trans_start 更新usbnet驱动网络设备watchdog用于监控网卡驱动发送数据是否异常,如果异常就报错,并调用网卡驱动提供的超时处理函数。基本原理:网卡设备初始化时,初始化watchdog定时器,用户空间打开网卡设备时,同时开启watchdog定时器,每次watchdog定时器超时,就检查网卡设备的发送队列发送数据是否超时,如果超时报错并调用网卡驱动提供的超时处理函数。然后重启watchdog定时器超时。linu_watchdog ixgbe transmit queue54 timed out

ubuntu系统使用SSH远程连接ROS小车并使用远程的Rviz_远程打开rviz-程序员宅基地

文章浏览阅读5.2k次,点赞9次,收藏90次。ubuntu系统SSH连接实现(账号密码)远程Rviz实现_远程打开rviz

随便推点

STM32窗口看门狗(警犬)_如果在while(1)循环体一直重设窗口看门狗值,系统会不会复位-程序员宅基地

文章浏览阅读632次。学习窗口看门狗记录_如果在while(1)循环体一直重设窗口看门狗值,系统会不会复位

如何优雅的实现 Spring Boot 接口参数加密解密?_encrypt-body-spring-boot-starter-程序员宅基地

文章浏览阅读5.1k次,点赞19次,收藏83次。因为有小伙伴刚好问到这个问题,松哥就抽空撸一篇文章和大家聊聊这个话题。加密解密本身并不是难事,问题是在何时去处理?定义一个过滤器,将请求和响应分别拦截下来进行处理也是一个办法,这种方式虽然粗暴,但是灵活,因为可以拿到一手的请求参数和响应数据。不过 SpringMVC 中给我们提供了 ResponseBodyAdvice 和 RequestBodyAdvice,利用这两个工具可以对请求和响应进行预处理,非常方便。所以今天这篇文章有两个目的:分享参数/响应加解密的思路。分享 ResponseBodyA_encrypt-body-spring-boot-starter

程序使用微软雅黑作为默认字体在xp下的问题_xp默认字体变成微软雅黑了-程序员宅基地

文章浏览阅读2.4k次。1、微软雅黑是 Vista 及更高版本 Windows 的标配中文字体,但不是 XP 的标配字体,XP 的任何一个 SP 或更新包都没有包含它。2、XP系统默认中文字体是宋体,在电脑上的显示效果不如微软雅黑好看。3、程序如果在XP下使用微软雅黑,但系统又没有该字体时,会使用XP默认字体。4、XP 系统上的微软雅黑字体,通常有两种来源:用户主动下载安装或安装 Of_xp默认字体变成微软雅黑了

iOS 浅谈MVVM+RAC_ioss中mvvm使用raccommand请求多个接口-程序员宅基地

文章浏览阅读788次。学习笔记之MVVM+RAC公司项目之前的很多年一直是用MVC框架,最近项目改版(加重构)提出了使用MVVM + RAC的框架结构,以达到各个部分模块代码之间的解耦。关于MVVM 以及RAC 还不太了解的同学请自行百度,我这里主要讲解下简单的使用。 以登录界面为例,需求如下: - 注册用户输入手机号密码登录 - 手机号获取验证码快速登录 - 游客登录 - 第三方(QQ,微信…)登录_ioss中mvvm使用raccommand请求多个接口

【复杂网络】复杂网络度的不相关性(Degree-uncorrelated network)标注笔记-程序员宅基地

文章浏览阅读4.5k次,点赞6次,收藏19次。【笔记】度相关、不相关网络的理解(不一定正确)社交网络中,你刚注册,系统给你推荐了小明,你关注小明的概率和小明一点关系都没有,纯粹是你为了完成任务随便选了个人关注。因为你关注小明的概率随机,只是由你自己的关注总数(节点出度)决定,和对方的关注数和被关注数无关。这个网络就是度不相关网络。你已经玩社交网络很久,系统根据你的兴趣给你推荐小明,你关注小明的概率和小明是不是大V有很大关系(假设..._uncorrelated network

Android中帧动画实现_android中多种动画形式帧动画-程序员宅基地

文章浏览阅读9.1k次。新建一个framebyframe.xml文件

推荐文章

热门文章

相关标签