使用MVP框架和WanAndroid API实现玩鸿蒙客户端

2024-03-29 15:08

本文主要是介绍使用MVP框架和WanAndroid API实现玩鸿蒙客户端,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

使用wanandroid api实现一个鸿蒙客户端

  • 前言
    • 网络请求
    • MVP框架封装
    • Fraction
      • Fraction的生命周期
      • Fraction的使用
    • TabList和PageSlider的组合使用
      • Tablist的使用
      • PageSlider的使用
    • Glide在鸿蒙OS中的使用
    • 自定义FlowLayout
    • AttrSet封装组件
    • WebView的使用
    • 缓存本地数据
      • Preference缓存本地数据
      • hawk缓存本地数据
    • 分布式数据库
    • 源码

前言

看了很久的鸿蒙开发文档,对鸿蒙开发有了一定的了解;那么就手动实现一个鸿蒙客户端,作为对文档知识的整合,主要使用wanandroid的api,实现了首页、项目页、分类页、我的、登录页、搜索页等功能。

对鸿蒙的常用布局DirectionalLayout、DependentLayout、StackLayout和常用组件做了一定的了解。

效果图如下:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

网络请求

项目使用的JavaUI,所以使用retorfit进行网络请求,导入资源包就可以直接使用。

implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
implementation 'io.reactivex.rxjava3:rxjava:3.1.3'
  1. 封装统一数据返回格式
public class BaseResult<T> implements Serializable {public String errorMsg;public int errorCode;public T data;
}
  1. 构建Log拦截器
    使用Log拦截器拦截数据,并打印日志。
public class LogInterceptor implements Interceptor {HiLogLabel label = new HiLogLabel(HiLog.LOG_APP,0x00201,"networkLabel");@Overridepublic Response intercept(Chain chain) throws IOException {Request original = chain.request();Request.Builder requestBuilder = original.newBuilder();Request request = requestBuilder.build();String httpUrl = request.url().toString();Response response = chain.proceed(request);ResponseBody responseBody = response.peekBody(1024 * 1024);if (httpUrl.toString().contains(".png")|| httpUrl.toString().contains(".jpg")|| httpUrl.toString().contains(".jpeg")|| httpUrl.toString().contains(".gif")) {return response;}String result = responseBody.string();HiLog.debug(label,"请求Headers>>"+request.headers().toString());HiLog.debug(label,"请求Url>>"+httpUrl);HiLog.debug(label,"请求结果>>>"+result);return response;}
}
  1. 构建Cookie拦截器
    构建Cookie拦截器,保存和读取cookie,并将cookie添加到请求头。

保存cookie:

public class SaveCookieInterceptor implements Interceptor {@Overridepublic Response intercept(Chain chain) throws IOException {Response originalResponse = chain.proceed(chain.request());//这里获取请求返回的cookieif (!originalResponse.headers("Set-Cookie").isEmpty()) {HashSet<String> cookies = new HashSet<>();for(String header: originalResponse.headers("Set-Cookie")){cookies.add(header);}Hawk.put("cookieData",cookies);}return originalResponse;}
}

读取cookie并添加到请求头:

public class ReadCookieInterceptor implements Interceptor {@Overridepublic Response intercept(Chain chain) throws IOException {Request.Builder builder = chain.request().newBuilder();HashSet<String> perferences = Hawk.get("cookieData");if (perferences != null) {for (String cookie : perferences) {builder.addHeader("Cookie", cookie);}}return chain.proceed(builder.build());}
}
  1. 构建Client
public class RetrofitUtil {public static volatile RetrofitUtil instance;private RequestService service;private Retrofit retrofit;private OkHttpClient okHttpClient;public static RetrofitUtil getInstance(){if (instance == null){synchronized (RetrofitUtil.class){if (instance == null) {instance = new RetrofitUtil();}}}return instance;}public OkHttpClient getOkHttpClient(){if (okHttpClient == null){okHttpClient = new OkHttpClient().newBuilder().addInterceptor(new LogInterceptor()).addInterceptor(new SaveCookieInterceptor()).addInterceptor(new ReadCookieInterceptor()).connectTimeout(10, TimeUnit.SECONDS).readTimeout(20, TimeUnit.SECONDS).retryOnConnectionFailure(false).build();}return okHttpClient;}public RequestService getService(){if (retrofit == null){retrofit = new Retrofit.Builder().baseUrl(new UrlContent().BaseUrl).addConverterFactory(GsonConverterFactory.create()).client(getOkHttpClient()).build();}if (service == null){service = retrofit.create(RequestService.class);}return service;}
}
  1. 构建Service
    构建service封装请求。
public interface RequestService {@GET("/banner/json")Call<BaseResult<List<BannerModule>>> getBanner();@GET("/article/list/{page}/json")Call<BaseResult<ArticleModule>> getArticle(@Path("page") int page);@GET("/project/tree/json")Call<BaseResult<List<ProjectTreeModule>>> getProjectTree();@GET("/project/list/{page}/json")Call<BaseResult<ArticleModule>> getProjectListByCid(@Path("page") int page,@Query("cid") int cid);@GET("/hotkey/json")Call<BaseResult<List<HotKeyModule>>> getHotKeyList();@GET("/navi/json")Call<BaseResult<List<NavJsonModule>>> getNavJson();@FormUrlEncoded@POST("/user/login")Call<BaseResult<Object>> login(@Field("username") String username,@Field("password") String password);@POST("/user/lg/userinfo/json")Call<BaseResult<UserConfigModule>> getUserInfo();@POST("/lg/collect/{cid}/json")Call<BaseResult<Object>> collectionArticle(@Path("cid") int cid);
}
  1. 使用
RetrofitUtil.getInstance().getService().getBanner()

MVP框架封装

Demo采用MVP框架进行开发。

鸿蒙推荐是在AbilitySlice和Fraction里面处理逻辑,不推荐在Ability里面处理逻辑,所以先创建AbilitySlice和Fraction的基类。

BaseAbilitySlice:

public abstract class BaseAbilitySlice extends AbilitySlice {public abstract int bindLayoutId();public abstract void initView();@Overrideprotected void onStart(Intent intent) {super.onStart(intent);super.setUIContent(bindLayoutId());initView();}public String getString(int resId) {try {return getResourceManager().getElement(resId).getString();} catch (Exception e) {e.printStackTrace();}return "";}public int getColor(int colorId) {try {return getResourceManager().getElement(colorId).getColor();} catch (Exception e) {e.printStackTrace();}return 0;}public FractionManager getFractionManager() {Ability ability = getAbility();if (ability instanceof FractionAbility) {FractionAbility fractionAbility = (FractionAbility) ability;return fractionAbility.getFractionManager();}return null;}}  

BaseFraction:

public abstract class BaseFraction extends Fraction {protected Component mComponent;public abstract int getUIContent();public abstract void initComponent();@Overrideprotected Component onComponentAttached(LayoutScatter scatter, ComponentContainer container, Intent intent) {mComponent = scatter.parse(getUIContent(), container, false);initComponent();return mComponent;}@Overrideprotected void onStart(Intent intent) {super.onStart(intent);}@Overrideprotected void onActive() {super.onActive();}@Overrideprotected void onForeground(Intent intent) {super.onForeground(intent);}@Overrideprotected void onInactive() {super.onInactive();}@Overrideprotected void onStop() {super.onStop();}public void intentToAbility(String abilityName,String param){Intent intent = new Intent();if (!param.isEmpty()){intent.setParam("urlLink",param);}Operation operation = new Intent.OperationBuilder().withDeviceId("").withBundleName("com.yangchoi.hmdemo").withAbilityName(abilityName).build();intent.setOperation(operation);getFractionAbility().startAbility(intent);}
}  

基类创建好之后就是对MVP的封装了。

首先要封装基类的View,里面你可以自定义方法,比如显示loading,关闭loading,成功success,错误onfailure等等方法:

public interface BaseView {void showLoading();void hideLoading();void onFailure(String errorMsg);}

再封装Presenter的基类,里面实现绑定视图,销毁视图等操作:

public class BasePresenter<V extends BaseView> {protected V mView;//绑定视图public void bindView(V view){this.mView = view;}//解除视图绑定public void unBindView(){this.mView = null;}//是否已绑定视图public boolean isBindView(){return this.mView != null;}
}  

既然要使用MVP框架,那么肯定要创建MVP对应的AbilitySlice和Fraction的基类,继承Presenter并实现BaseView接口。

BaseMVPAbilitySlice:

public abstract class BaseMVPAbilitySlice <T extends BasePresenter> extends BaseAbilitySlice implements BaseView {protected T mPresenter;@Overrideprotected void onStart(Intent intent) {super.onStart(intent);}@Overrideprotected void onStop() {if (mPresenter != null){mPresenter.unBindView();}super.onStop();}
}  

BaseMVPFraction:

public abstract class BaseMVPFraction <T extends BasePresenter> extends BaseFraction implements BaseView {protected T mPresenter;@Overrideprotected void onStop() {if (mPresenter != null){mPresenter.unBindView();}super.onStop();}
}  

使用步骤:

1.先创建对应的Contract,以下都以HomeFraction为例子:

public interface HomeFractionContract {//todo    
}  

创建model对应接口以及接口参数:

interface HomeModel{Call<BaseResult<List<BannerModule>>> getHomeBanner();Call<BaseResult<ArticleModule>> getArticleList(int page);
}

创建view继承至BaseView,实现自己自定义的方法:

interface View extends BaseView{@Overridevoid showLoading();@Overridevoid hideLoading();@Overridevoid onFailure(String errorMsg);//获取banner成功的回调接口void onBannerSuccess(List<BannerModule> bannerList);//获取文章列表成功的回调接口void onArticleSuccess(ArticleModule articleModule);
}  

创建presenter定义方法:

interface Presenter{void getHomeBanner();void getArticleList(int page);
}  

所以整个Contract的代码如下:

public interface HomeFractionContract {interface HomeModel{Call<BaseResult<List<BannerModule>>> getHomeBanner();Call<BaseResult<ArticleModule>> getArticleList(int page);}interface View extends BaseView{@Overridevoid showLoading();@Overridevoid hideLoading();@Overridevoid onFailure(String errorMsg);void onBannerSuccess(List<BannerModule> bannerList);void onArticleSuccess(ArticleModule articleModule);}interface Presenter{void getHomeBanner();void getArticleList(int page);}
}  

2.创建对应的Model,实现Contactmodel里面定义的方法并与api service进行关联:

public class HomeFractionModel implements HomeFractionContract.HomeModel {@Overridepublic Call<BaseResult<List<BannerModule>>> getHomeBanner() {return RetrofitUtil.getInstance().getService().getBanner();}@Overridepublic Call<BaseResult<ArticleModule>> getArticleList(int page) {return RetrofitUtil.getInstance().getService().getArticle(page);}
} 

3.创建对应的Presenter,通过model关联指定方法进行网络请求,并通过view将返回的结果回调到对应方法上:

public class HomeFractionPresenter extends BasePresenter<HomeFractionContract.View> implements HomeFractionContract.Presenter {private HomeFractionContract.HomeModel homeModel;public HomeFractionPresenter() {homeModel = new HomeFractionModel();}@Overridepublic void getHomeBanner() {if (!isBindView())return;mView.showLoading();//通过model指定方法进行请求homeModel.getHomeBanner().enqueue(new Callback<BaseResult<List<BannerModule>>>() {@Overridepublic void onResponse(Call<BaseResult<List<BannerModule>>> call, Response<BaseResult<List<BannerModule>>> response) {mView.hideLoading();if (response.body().getErrorCode() == 0){//请求成功将数据回调到成功的方法mView.onBannerSuccess(response.body().getData());}}@Overridepublic void onFailure(Call<BaseResult<List<BannerModule>>> call, Throwable throwable) {mView.hideLoading();//请求失败将错误信息回调到失败的方法mView.onFailure(throwable.getMessage());}});}@Overridepublic void getArticleList(int page) {if (!isBindView())return;mView.showLoading();homeModel.getArticleList(page).enqueue(new Callback<BaseResult<ArticleModule>>() {@Overridepublic void onResponse(Call<BaseResult<ArticleModule>> call, Response<BaseResult<ArticleModule>> response) {mView.hideLoading();if (response.body().getErrorCode() == 0){mView.onArticleSuccess(response.body().getData());}}@Overridepublic void onFailure(Call<BaseResult<ArticleModule>> call, Throwable throwable) {mView.hideLoading();mView.onFailure(throwable.getMessage());}});}
} 

4.Fraction或者AbilitySlice继承BaseMVPFraction或者BaseMVPAbilitySlice,实现view的回调接口处理数据,通过presenter请求数据:

public class HomeFraction extends BaseMVPFraction<HomeFractionPresenter> implements HomeFractionContract.View {private PageSlider pageSlider;private int currentPage = 0;private ListContainer listContainer;private int requestPage = 0;private PullRefreshLayout refreshView;private int pageCount = 0;EventHandler eventHandler = new EventHandler(EventRunner.getMainEventRunner());private HomeFractionPresenter presenter;@Overridepublic int getUIContent() {return ResourceTable.Layout_fraction_home;}@Overridepublic void initComponent() {pageSlider = (PageSlider) mComponent.findComponentById(ResourceTable.Id_page_slider);listContainer = (ListContainer) mComponent.findComponentById(ResourceTable.Id_list_container);refreshView = (PullRefreshLayout) mComponent.findComponentById(ResourceTable.Id_refresh_view);presenter = new HomeFractionPresenter();presenter.bindView(this);presenter.getHomeBanner();presenter.getArticleList(requestPage);}@Overridepublic void showLoading() {}@Overridepublic void hideLoading() {}@Overridepublic void onFailure(String errorMsg) {}@Overridepublic void onBannerSuccess(List<BannerModule> bannerList) {}@Overridepublic void onArticleSuccess(ArticleModule articleModule) {}}   

4.1:初始化Presenter,并绑定视图:

presenter = new HomeFractionPresenter();
presenter.bindView(this);

4.2:请求数据

presenter.getHomeBanner();
presenter.getArticleList(requestPage);    

4.3:重写成功的方法处理数据:

@Override
public void onBannerSuccess(List<BannerModule> bannerList) {pageSlider.setProvider(new BannerProvider(bannerList,getContext()));pageSlider.setSlidingPossible(true);pageSlider.addPageChangedListener(new PageSlider.PageChangedListener() {@Overridepublic void onPageSliding(int i, float v, int i1) {}@Overridepublic void onPageSlideStateChanged(int i) {}@Overridepublic void onPageChosen(int i) {}});pageSlider.setCurrentPage(currentPage,true);
} 

4.4:重写失败的方法处理异常:

@Override
public void onFailure(String errorMsg) {//todo
}

以上就是MVP的封装和使用了,还是很简单的。

Fraction

Fraction是Ability的一部分,不能单独使用;用法有点类似于android的fragment,承载于Activity;Fration同样承载于Ability,如果Ability被销毁,那么Fraction也会被销毁,所以看得出来Fraction的声明周期由承载它的容器决定。

Fraction的生命周期

Fraction生命周期主要涉及到八个生命周期方法,分别是onComponentAttached、onStart、onActive、onInactive、onBackground、onForeground、onStop、onComponentDetach。

启动的时候依次执行onComponentAttached、onStart、onActive

当程序进入后台依次调用onInactive、onBackground

重新回到前台依次调用onForeground、onActive

退出fraction依次调用onInactive、onBackground、onStop、onComponentDetach。

Fraction的使用

想要使用Fraction首先要让承载Fraction的容器继承FractionAbility,这么做的目的是为了拿到FractionManager来管理Fration。

容器继承FractionAbility :

public class FractionAbility extends FractionAbility {@Overridepublic void onStart(Intent intent) {super.onStart(intent);super.setMainRoute(TestFractionAbilitySlice.class.getName());}
}

创建布局文件使用StackLayout:

<?xml version="1.0" encoding="utf-8"?>
<DirectionalLayoutxmlns:ohos="http://schemas.huawei.com/res/ohos"ohos:height="match_parent"ohos:orientation="vertical"ohos:background_element="$graphic:background_ability_main"ohos:width="match_parent"><StackLayoutohos:id="$+id:test_fraction"ohos:width="match_parent"ohos:height="match_parent"/></DirectionalLayout>

创建Fraction的布局文件:

<?xml version="1.0" encoding="utf-8"?>
<DirectionalLayoutxmlns:ohos="http://schemas.huawei.com/res/ohos"ohos:height="match_parent"ohos:orientation="vertical"ohos:background_element="$graphic:background_ability_main"ohos:width="match_parent"><Textohos:background_element="$graphic:background_ability_main"ohos:height="match_content"ohos:layout_alignment="horizontal_center"ohos:text="home fraction"ohos:text_size="50"ohos:width="match_content"/></DirectionalLayout>

创建Fraction:

public class TestFraction extends Fraction {@Overrideprotected Component onComponentAttached(LayoutScatter scatter, ComponentContainer container, Intent intent) {Component component = scatter.parse(ResourceTable.Layout_test_fraction, container, false);return component;}@Overrideprotected void onStart(Intent intent) {super.onStart(intent);}
}

在AbilitySlice里面调用getAbility方法,将其强转为FractionAbility,然后调用getFractionManager方法得到FractionManager对象,再调用startFractionScheduler方法开启事务,调用add方法添加fraction,add方法第一个参数就是我们在布局文件中添加的栈布局的id,第二个参数就是TestFraction对象,第三个参数就是给TestFraction设置一个标签,后续可以FractionManager的getFractionByTag方法,来查找我们已经创建好的HomeFraction对象,getFractionByTag方法的参数就是我们在add方法里面给TestFraction设置的标签。最后提交事务。

public class TestFractionAbilitySlice extends AbilitySlice {@Overrideprotected void onStart(Intent intent) {super.onStart(intent);setUIContent(ResourceTable.Layout_ability_test_fraction);((FractionAbility)getAbility()).getFractionManager().startFractionScheduler().add(ResourceTable.Id_test_fraction, new TestFraction()).submit();}
}

管理Fraction的方法如下:

public abstract class FractionScheduler {public FractionScheduler() {throw new RuntimeException("Stub!");}public abstract FractionScheduler add(int var1, Fraction var2);public abstract FractionScheduler add(int var1, Fraction var2, String var3);public abstract FractionScheduler replace(int var1, Fraction var2);public abstract FractionScheduler remove(Fraction var1);public abstract FractionScheduler hide(Fraction var1);public abstract FractionScheduler show(Fraction var1);public abstract int submit();public abstract FractionScheduler pushIntoStack(String var1);
}

TabList和PageSlider的组合使用

TabList主要用于实现菜单栏,比如顶部或者底部的菜单栏都可以使用TabList进行实现,使用TabList的时候要使用Tab作为子标签,意思就是往TabList里面添加元素要添加Tab。

Tablist的使用

xml添加TabList,并设置相关属性:

<TabListohos:id="$+id:tab_list_view"ohos:height="50vp"ohos:width="match_parent"ohos:left_margin="10vp"ohos:right_margin="10vp"ohos:orientation="horizontal"ohos:normal_text_color="$color:tab_unselect_color"ohos:selected_text_color="$color:tab_select_color"ohos:selected_tab_indicator_color="$color:tab_select_color"ohos:selected_tab_indicator_height="2vp"ohos:tab_indicator_type="bottom_line"ohos:tab_margin="10vp"ohos:text_alignment="center"ohos:text_size="16vp"ohos:background_element="$graphic:shape_white_bg"/>

获取实例:

private TabList tabList;
tabList = (TabList) mComponent.findComponentById(ResourceTable.Id_tab_list_view);

循环添加数据:

for (int i = 0; i < list.size(); i++) {if (i == 0){tab = tabList.new Tab(getContext());tab.setText(list.get(i).name);cid = list.get(i).id;tabList.addTab(tab);}else {TabList.Tab tab = tabList.new Tab(getContext());tab.setText(list.get(i).name);tabList.addTab(tab);}if (i >= list.size() -1){tabList.selectTab(tab);//设置默认选中tab,不设置该选项的话tablist默认定位到最后一个tab}
}

这里有一点需要注意,第一个tab要单独拿出来,当元素添加完之后要使用 tabList.selectTab默认选中第一个tab,否则TabList的数据会默认定位到最后一条。

监听tab选中事件:

tabList.addTabSelectedListener(new TabList.TabSelectedListener() {@Overridepublic void onSelected(TabList.Tab tab) {pageSlider.setCurrentPage(tab.getPosition());//关联pageSlider}@Overridepublic void onUnselected(TabList.Tab tab) {}@Overridepublic void onReselected(TabList.Tab tab) {}
});

在onSelected选中的方法里面绑定pageSlider, 达到当点击tablist的tab的时候,pageSlider会发生变化。

PageSlider的使用

PageSlider是用于页面之间切换的组件,它通过响应滑动事件完成页面间的切换。

xml布局创建:

<PageSliderohos:id="$+id:page_slider"ohos:height="200vp"ohos:width="match_parent"/>

获取实例:

pageSlider = (PageSlider) mComponent.findComponentById(ResourceTable.Id_page_slider);

创建Provider,在createPageInContainer方法中创建并添加子组件:

public class BannerProvider extends PageSliderProvider {private List<BannerModule> dataList;private Context mContext;public BannerProvider(List<BannerModule> dataList, Context mContext) {this.dataList = dataList;this.mContext = mContext;}@Overridepublic int getCount() {return dataList.size();}@Overridepublic Object createPageInContainer(ComponentContainer componentContainer, int i) {Image image = new Image(mContext);image.setWidth(ComponentContainer.LayoutConfig.MATCH_CONTENT);image.setHeight(ComponentContainer.LayoutConfig.MATCH_CONTENT);image.setScaleMode(Image.ScaleMode.ZOOM_CENTER);Glide.with(mContext).load(dataList.get(i).imagePath).into(image);componentContainer.addComponent(image);return image;}@Overridepublic void destroyPageFromContainer(ComponentContainer componentContainer, int i, Object o) {componentContainer.removeComponent((Component) o);}@Overridepublic boolean isPageMatchToObject(Component component, Object o) {return component == o;}
}

绑定Provider:

pageSlider.setProvider(new BannerProvider(bannerModuleList,getContext()));

监听滑动,并绑定Tablist:

pageSlider.addPageChangedListener(new PageSlider.PageChangedListener() {@Overridepublic void onPageSliding(int itemPos, float itemPosOffset, int itemPosPixles) { }@Overridepublic void onPageSlideStateChanged(int state) { }@Overridepublic void onPageChosen(int itemPos) { //连接Tabif(i != tabList.getSelectedTabIndex()){tabList.selectTabAt(i);}}
});

在onPageChosen方法关联TabList。

当TabList和PagerSlide相互关联之后,就可以达到微信的滑动切换页面效果。

Glide在鸿蒙OS中的使用

Glide作为最常用的图片加载框架,同样支持在鸿蒙OS里面使用了,步骤如下:

1.build.gradle添加maven:

allprojects {repositories {//打开mavenmavenCentral()//.....}
}

2.entry的build.gradle导入相关依赖包:

implementation 'io.openharmony.tpc.thirdlib:glide:1.1.3'

3.使用Glide加载图片:

Glide.with(mContext).load(url).into(image);  

4.使用Glide加载圆形图片:

Glide.with(getContext()).load(uri).circleCrop().into(userImage);

5.使用Glide给图片设置角度:

Glide.with(getContext()).load(uri).transform( new RoundedCorners(180)).into(userImage);

加载圆形和设置角度不能同时使用,因为圆形也是给图片设置了角度,再设置角度就会导致没效果。

自定义FlowLayout

自定义流式布局是最简单,也是最能体现一个自定义view的操作过程的,鸿蒙也是支持自定义布局的。

一个自定义view的步骤如下:

1.继承ComponentContainer并实现Component.EstimateSizeListener, ComponentContainer.ArrangeListener接口:

public class FlowLayout extends ComponentContainer implements Component.EstimateSizeListener, ComponentContainer.ArrangeListener{//todo
}
  • Component.EstimateSizeListener 对组件测量宽高
  • ComponentContainer.ArrangeListener 摆放子组件

2.测量模式
在EstimateSpec类下我们可以看到组件的测量模式:

  • EstimateSpec.UNCONSTRAINT UNCONSTRAINT的意思是不受约束。父组件对子组件没有约束,表示子组件可以任意大小。一般用于系统内部,或者ListContainer、ScrollView等组件。
  • EstimateSpec.PRECISE PRECISE是精确的意思。父组件已确定子组件的宽高。在PRECISE模式下,组件的测量大小就是通过EstimateSpec.getSize方法得到的数值。
  • EstimateSpec.NOT_EXCEED NOT_EXCEED的意思是不超过。父组件没有确定子组件的宽高,但父组件已为子组件指定了最大的宽高,子组件不能超过指定的宽高。

3.onEstimateSize测量宽高

  • int widthMode = EstimateSpec.getMode(widthEstimatedConfig); 得到宽度的测量模式
  • int heightMode = EstimateSpec.getMode(heightEstimatedConfig); 得到高度的测量模式
  • int width = EstimateSpec.getSize(widthEstimatedConfig); 测量到宽度的大小
  • int height = EstimateSpec.getSize(heightEstimatedConfig); 测量到高度的大小

根据测量模式测量宽高:

if (widthMode == EstimateSpec.PRECISE && heightMode == EstimateSpec.PRECISE) {// 此时流式布局的宽高就是调用EstimateSpec.getSize方法得到的数值mEstimateFlowLayoutWidth = width;mEstimateFlowLayoutHeight =  height;// 在精确模式下测量子组件的宽高estimateChildByPrecise(width, widthEstimatedConfig, heightEstimatedConfig);
} else {// 不是精确模式,测量流式布局的宽高和子组件的宽高estimateChildByNotExceed(widthEstimatedConfig, heightEstimatedConfig, width);
}

测量子组件,当子组件的宽度总和大于流式布局的宽度,那么换行,否则直接添加到流式布局:

private void estimateChildByPrecise(int width, int widthEstimatedConfig, int heightEstimatedConfig) {int childWidth;int childHeight;int curLineWidth = 0;int curLineHeight = 0;int childCount = getChildCount();LogUtil.debugLog("childCount:"+childCount);List<Component> lineComponent = new ArrayList<>();for (int i = 0; i < childCount; i++) {Component child = getComponentAt(i);if (child.getVisibility() == HIDE) {continue;}LayoutConfig layoutConfig = child.getLayoutConfig();int childWidthMeasureSpec = EstimateSpec.getChildSizeWithMode(layoutConfig.width, widthEstimatedConfig, EstimateSpec.UNCONSTRAINT);int childHeightMeasureSpec = EstimateSpec.getChildSizeWithMode(layoutConfig.height, heightEstimatedConfig, EstimateSpec.UNCONSTRAINT);child.estimateSize(childWidthMeasureSpec, childHeightMeasureSpec);childWidth = child.getEstimatedWidth() + layoutConfig.getMarginLeft() + layoutConfig.getMarginRight();childHeight = child.getEstimatedHeight() + layoutConfig.getMarginTop() + layoutConfig.getMarginBottom();if (childWidth + curLineWidth > width) {lineHeight.add(curLineHeight);listLineComponent.add(lineComponent);curLineWidth = childWidth;curLineHeight = childHeight;lineComponent = new ArrayList<>();} else {curLineWidth += childWidth;curLineHeight = Math.max(curLineHeight, childHeight);}lineComponent.add(child);if (i == childCount - 1) {lineHeight.add(curLineHeight);listLineComponent.add(lineComponent);}}
}

4.onArrange摆放子组件

public boolean onArrange(int left, int top, int width, int height) {// 当前行到流式布局左边的距离int curLineLeft = 0;// 当前行到流式布局上边的距离int curLineTop = 0;// 子组件左上角到流式布局左边的距离int l;// 子组件左上角到流式布局上边的距离int t;/** 我们需要把子组件一行一行的显示出来,之前用集合存储了每行的子组件,此时就可以遍历了,* 先拿到每行的子组件,再遍历每行中具体的一个子组件*/for (int i = 0; i < listLineComponent.size(); i++) {// 得到每行的子组件List<Component> lineComponent = listLineComponent.get(i);// 得到每行中具体的一个子组件for (Component component : lineComponent) {if (component.getVisibility() == Component.HIDE) {// 子组件隐藏了,不做处理continue;}LayoutConfig layoutConfig = component.getLayoutConfig();// 得到子组件的左边的外边距int marginLeft = layoutConfig.getMarginLeft();// 得到子组件的上边的外边距int marginTop = layoutConfig.getMarginTop();// 得到子组件的右边的外边距int marginRight = layoutConfig.getMarginRight();// 得到子组件测量后的宽度int estimatedWidth = component.getEstimatedWidth();// 得到子组件测量后的高度int estimatedHeight = component.getEstimatedHeight();// 子组件左上角到流式布局左边的距离 = 当前行到流式布局左边的距离 + 子组件的左边的外边距l = curLineLeft + marginLeft;// 子组件左上角到流式布局上边的距离 = 当前行到流式布局上边的距离 + 子组件的上边的外边距t = curLineTop + marginTop;// 计算完子组件的左上以及子组件的宽高,调用arrange方法确定子组件的位置component.arrange(l, t, estimatedWidth, estimatedHeight);// 更新当前行到流式布局左边的距离,当前行到流式布局左边的距离 = 子组件测量后的宽度 + 子组件的左边的外边距 + 子组件的右边的外边距curLineLeft += estimatedWidth + marginLeft + marginRight;}// 遍历完一行后,当前行到流式布局左边的距离就要为0,因为又要从头开始了curLineLeft = 0;// 遍历完一行后,当前行到流式布局上边的距离 = 前行到流式布局上边的距离 + 当前行的高度,因为开始在下一行确定子组件的摆放位置了curLineTop += lineHeight.get(i);}return true;
}

5.使用
xml布局添加FlowLayout:

<com.yangchoi.hmdemo.view.FlowLayoutohos:id="$+id:flow_layout"ohos:height="match_content"ohos:width="match_parent"/>

循环添加数据:

for (NavJsonChildModule module : dataList){Text text = (Text) LayoutScatter.getInstance(context).parse(ResourceTable.Layout_item_text, null, false);text.setText(module.getTitle());addComponent(text);
}

这里有一点需要注意,当我们添加过一次数据之后FlowLayout已经有了子view,那么它就有了高度,如果我们重新添加新数据的时候,FlowLayout就会将新数据在原有高度上继续添加,但是原来的数据并没有显示,所以在添加新数据的时候要对FlowLayout的数据做一下清空。

清除数据源所有数据:

listLineComponent.clear();

移除FlowLayout所有子view:

removeAllComponents();

重置FlowLayout的高度:

mEstimateFlowLayoutHeight = height;

所以添加新数据的方法应该是这样子的:

public void setFlowLayoutNewData(int height,List<NavJsonChildModule> dataList,Context context){mEstimateFlowLayoutHeight = height;listLineComponent.clear();removeAllComponents();for (NavJsonChildModule module : dataList){Text text = (Text) LayoutScatter.getInstance(context).parse(ResourceTable.Layout_item_text, null, false);text.setText(module.getTitle());addComponent(text);}
}

给大家安利一个链接,戳我戳我~~~鸿蒙系统自定义流式布局,本文适合初学者
一个大佬的自定义FlowLayout,对鸿蒙自定义的分析非常到位,注释很全面,直接让人爱了,更适合对鸿蒙自定义view的学习;有兴趣的可以看一下。

AttrSet封装组件

学习和了解一下在鸿蒙里面使用AttrSet属性标签,顶部菜单栏的封装主要设置组件属性AttrSet,和android设置AttributeSet一个道理,只是HarmonyOS不需要在xml里面设置属性标签,直接在引用组件的布局设置就可以了。

首先创建ToolbarLayout继承至DependentLayout:

public class ToolbarLayout extends DependentLayout {private Image backImage;private Text menuTitle;private Image rightImage;public ToolbarLayout(Context context) {super(context);}public ToolbarLayout(Context context, AttrSet attrSet) {super(context, attrSet);initView(context, attrSet);}public ToolbarLayout(Context context, AttrSet attrSet, String styleName) {super(context, attrSet, styleName);initView(context, attrSet);}
}

在initView方法中获取布局文件,并根据属性标签显示不同内容:

private void initView(Context context,AttrSet attrSet){Component component = LayoutScatter.getInstance(context).parse(ResourceTable.Layout_layout_top_toolbar,null,false);backImage = (Image) component.findComponentById(ResourceTable.Id_backImage);menuTitle = (Text) component.findComponentById(ResourceTable.Id_title);rightImage = (Image) component.findComponentById(ResourceTable.Id_rightImage);// get attr set// 是否显示左边的返回按钮boolean isShowLeft = attrSet.getAttr("isShowLeft").get().getBoolValue();//标题内容String titleStr = attrSet.getAttr("title").get().getStringValue();// 是否显示右边操作按钮boolean isShowRight = attrSet.getAttr("isShowRight").get().getBoolValue();// 右边操作按钮的内容,根据不同内容进行不同操作String rightContent = attrSet.getAttr("rightContent").get().getStringValue();if (!isShowLeft){backImage.setVisibility(HIDE);}else {backImage.setClickedListener( component1 -> {if (context instanceof Ability){LogUtil.debugLog("finish");context.terminateAbility();}else {LogUtil.debugLog("fraction无效点击");}});}if (!isShowRight){rightImage.setVisibility(HIDE);}else {rightImage.setClickedListener( component1 -> {switch (rightContent){case "搜索":intentAbility(context);break;}});}menuTitle.setText(titleStr);addComponent(component);
}

通过attrSet.getAttr(“isShowLeft”).get().getBoolValue()获取标签内容,isShowLeft是标签的名字,getBoolValue是标签的数据类型。

组件封装好了那自然就是要使用组件了,鸿蒙OS在使用AttrSet的时候不需要在xml构建标签属性,只需要添加app的命名控件就可以直接使用了:

xmlns:app="http://schemas.huawei.com/apk/res/ohos"

添加xmlns:app命名控件之后就可以直接在布局文件里面使用了:
比如下面布局的app:title=“首页”,app:isShowRight="true"等。

<?xml version="1.0" encoding="utf-8"?>
<DirectionalLayoutxmlns:ohos="http://schemas.huawei.com/res/ohos"xmlns:app="http://schemas.huawei.com/apk/res/ohos"ohos:height="match_parent"ohos:width="match_parent"ohos:background_element="$graphic:shape_bg_layout"ohos:orientation="vertical"><com.yangchoi.hmdemo.view.ToolbarLayoutohos:height="match_content"ohos:width="match_parent"app:isShowLeft="false"app:title="首页"app:isShowRight="true"app:rightContent="搜索"/>
</DirectionalLayout>

WebView的使用

鸿蒙OS同样支持使用Webview加载H5,但是有一个小坑。

一般我们在使用webview的时候,xml里面是这样子的:

<?xml version="1.0" encoding="utf-8"?><DirectionalLayoutxmlns:ohos="http://schemas.huawei.com/res/ohos"xmlns:app="http://schemas.huawei.com/apk/res/ohos"ohos:height="match_parent"ohos:width="match_parent"ohos:alignment="center"ohos:orientation="vertical"><WebViewohos:id="$+id:webview"ohos:height="match_parent"ohos:width="match_parent"/></DirectionalLayout>

看着好像没什么问题,webview也添加上了;哎~恭喜你,出错了,这种看着没什么问题,但是运行起来的时候webview会抛出无法找到对象的异常。

所以正确的布局文件应该是这样子的:

<?xml version="1.0" encoding="utf-8"?><DirectionalLayoutxmlns:ohos="http://schemas.huawei.com/res/ohos"xmlns:app="http://schemas.huawei.com/apk/res/ohos"ohos:height="match_parent"ohos:width="match_parent"ohos:alignment="center"ohos:orientation="vertical"><ohos.agp.components.webengine.WebViewohos:id="$+id:webview"ohos:height="match_parent"ohos:width="match_parent"/></DirectionalLayout>

记得加上前面的包名ohos.agp.components.webengine,在编写Webview的时候没有就自己添加上,复制过来添加上都可以。

AbilitySlice使用WebView:

public class H5AbilitySlice extends AbilitySlice {private WebView webView;private String link = "";@Overridepublic void onStart(Intent intent) {super.onStart(intent);super.setUIContent(ResourceTable.Layout_ability_h5);webView = (WebView) findComponentById(ResourceTable.Id_webview);link = intent.getStringParam("urlLink");initWebView(link);}@Overridepublic void onActive() {super.onActive();}@Overridepublic void onForeground(Intent intent) {super.onForeground(intent);}private void initWebView(String link){webView.getWebConfig().setJavaScriptPermit(true);//支持jswebView.load(link);}@Overridepublic boolean onKeyDown(int keyCode, KeyEvent keyEvent) {return super.onKeyDown(keyCode, keyEvent);}
}

缓存本地数据

在项目开发过程中我们多少要使用到本地缓存,将一些数据缓存到本地,这里简单介绍一下鸿蒙提供的Preference和第三方库hawk缓存本地数据。

Preference缓存本地数据

鸿蒙本身也提供了Preference来作为数据缓存。
使用步骤如下:

1.通过DatabaseHelper对象获取实例:

DatabaseHelper databaseHelper = new DatabaseHelper(getContext()); 
String fileName = "fileName";
Preferences preferences = databaseHelper.getPreferences(fileName);

2.同步写入数据:

preferences.putString("StringKey", "String value");
preferences.flush();

3.异步写入数据

preferences.putString("StringKey", "String value");
bool result = preferences.flushSync();

4.读取数据

int value = preferences.getInt("intKey", 0);
String valuStr = preferences.getString("StringKey");

这里我遇到上下文对象的bug,比如我在MainAbility初始化Preference,并使用this传入上下文对象:

DatabaseHelper databaseHelper = new DatabaseHelper(this); 

我在其他Ability使用this传入上下文的时候就发生错误了;所以这里个人建议是都使用getContext()获取上下文对象,或者是使用application。

hawk缓存本地数据

这个demo里面我使用hawk这个第三方库来缓存本地数据,当然你也可以使用Preference,或者使用一些其他的第三方工具,比如腾讯的MMKV也支持鸿蒙OS了;这里只对hawk做简单的介绍。

1.在Application里面初始化:

Hawk.init(this).build();

2.以键值对的形式保存数据:

Hawk.put("cookieData",cookies);

3.读取数据:

String perferences = Hawk.get("cookieData");

4.删除指定key的数据:

Hawk.delete(key);

5.检查某个key是否存在:

Hawk.contains(key);

6.获取数据的总条数:

Hawk.count();

7.删除所有数据:

Hawk.deleteAll();

分布式数据库

在阅读文档的时候也对分布式数据库做了一下了解,这个项目也使用了分布式数据库,主要用于对搜索记录的保存,也算是对分布式数据库的一个实战了。

使用步骤如下:

1.config.json添加权限配置:

"reqPermissions": [{"name": "ohos.permission.DISTRIBUTED_DATASYNC"}
]

2.弹出授权提示框,请求用户进行授权:

requestPermissionsFromUser(new String[]{"ohos.permission.DISTRIBUTED_DATASYNC"},0);

3.获取Application上下文对象:

private static Context appContext; @Override
public void onInitialize() {super.onInitialize();appContext = getApplicationContext();
}public static Context getAppContext(){return appContext;
}

这也有一个上下文对象的bug,我在阅读文档的时候是在MainAbility里面操作的,并没有这个bug,但是我在项目里面的SearchAbility创建分布式数据库实例对象的时候就发现了这个bug,一直创建不成功,直到我改为application的上下文对象才创建成功;咱也不知道是为啥,知道的大佬告诉一下~

4.获取分布式数据库管理实例:

kvManagerConfig = new KvManagerConfig(MyApplication.getAppContext());
kvManager = KvManagerFactory.getInstance().createKvManager(kvManagerConfig);  

5.创建单版本分布式数据库:

try {Options options = new Options();options.setCreateIfMissing(true);options.setEncrypt(false);options.setKvStoreType(KvStoreType.SINGLE_VERSION);options.setSchema(schema);singleKvStore = kvManager.getKvStore(options, dbStoreId);
} catch (KvStoreException e) {LogUtil.errorLog("getKvStore:" + e.getKvStoreErrorCode() + " --:" + e.getMessage());
}

6.创建监听数据变化的方法:

private class kvStoreObserverListener implements KvStoreObserver{@Overridepublic void onChange(ChangeNotification changeNotification) {System.out.println("发生变化,onChange被回调");//新增的数据List<Entry> insertEntries = changeNotification.getInsertEntries();//发生改变的数据List<Entry> updateEntries = changeNotification.getUpdateEntries();//删除的数据List<Entry> deleteEntries = changeNotification.getDeleteEntries();//发生变化的设备idString deviceId = changeNotification.getDeviceId();}
}  

7.注册监听:

kvStoreObserverListener kvStoreObserverListener = new kvStoreObserverListener();
singleKvStore.subscribe(SubscribeType.SUBSCRIBE_TYPE_ALL,kvStoreObserverListener);    

8.构建谓词字段:

FieldNode fieldContent = new FieldNode("content");//搜索内容
fieldContent.setType(FieldValueType.STRING);//String类型
fieldContent.setNullable(false);//字段不能为空  

9.创建schema:

Schema schema = new Schema();
ArrayList<String> indexList = new ArrayList<>();
indexList.add("$.content");
indexList.add("$.longtime");
schema.setIndexes(indexList);  

10.创建关联:

schema.getRootFieldNode().appendChild(fieldContent);
schema.getRootFieldNode().appendChild(fieldTime);  

11.设置schema模式:

schema.setSchemaMode(SchemaMode.COMPATIBLE);

12.options设置schema:

options.setSchema(schema);

13.往分布式数据库写入数据:

//插入数据
private void kvEditData(String content){try {long time = System.currentTimeMillis();String key = "searchContent"+time;String timeNow = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(time);String value = "{\"content\":\""+content+"\",\"longtime\":"+time+",\"stringtime\":\""+timeNow+"\"}";singleKvStore.putString(key,value);KvSyncData();}catch (Exception e){LogUtil.errorLog("insert error:"+e.getMessage());}
}

14.从分布式数据库读取数据:

//查询数据
private void kvQueryData(){try {Query select = Query.select();//获取查询条件select.orderByAsc("$.longtime");//内容升序排序List<Entry> entries = singleKvStore.getEntries(select);if (entries != null && entries.size() > 0){for (Entry entry: entries){System.out.println("query success:" + entry.getValue().getString());SearchHistoryModule module = new Gson().fromJson(entry.getValue().getString(),SearchHistoryModule.class);Text text = (Text) LayoutScatter.getInstance(SearchAbilitySlice.this).parse(ResourceTable.Layout_item_text, null, false);if (module != null && !TextUtils.isEmpty(module.getContent())) {// 显示组件的内容text.setText(module.getContent());// 将组件添加到流式布局historyFlowLayout.addComponent(text);}}}}catch (Exception e){LogUtil.errorLog("query error:"+e.getMessage());}
}

15.同步数据:

//同步数据
private void KvSyncData(){try {List<ohos.data.distributed.device.DeviceInfo> deviceInfoList = kvManager.getConnectedDevicesInfo(DeviceFilterStrategy.NO_FILTER);List<String> deviceIdList = new ArrayList<>();for (ohos.data.distributed.device.DeviceInfo deviceInfo : deviceInfoList) {deviceIdList.add(deviceInfo.getId());}singleKvStore.sync(deviceIdList, SyncMode.PUSH_ONLY);System.out.println("sync db success");}catch (Exception e){System.out.println("sync db error:"+e.getMessage());}
}      

以上就是Demo里面对分布式数据库的使用,更多用法可以查看我的这篇文章HarmonyOS分布式数据服务

以上就是这客户端实现的功能了,还有很多功能是没有实现的,比如DataAbility,文件操作等,后续会慢慢添加。

源码

以上源码已上传至gitee。
戳我戳我~

这篇关于使用MVP框架和WanAndroid API实现玩鸿蒙客户端的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/858911

相关文章

中文分词jieba库的使用与实景应用(一)

知识星球:https://articles.zsxq.com/id_fxvgc803qmr2.html 目录 一.定义: 精确模式(默认模式): 全模式: 搜索引擎模式: paddle 模式(基于深度学习的分词模式): 二 自定义词典 三.文本解析   调整词出现的频率 四. 关键词提取 A. 基于TF-IDF算法的关键词提取 B. 基于TextRank算法的关键词提取

使用SecondaryNameNode恢复NameNode的数据

1)需求: NameNode进程挂了并且存储的数据也丢失了,如何恢复NameNode 此种方式恢复的数据可能存在小部分数据的丢失。 2)故障模拟 (1)kill -9 NameNode进程 [lytfly@hadoop102 current]$ kill -9 19886 (2)删除NameNode存储的数据(/opt/module/hadoop-3.1.4/data/tmp/dfs/na

Hadoop数据压缩使用介绍

一、压缩原则 (1)运算密集型的Job,少用压缩 (2)IO密集型的Job,多用压缩 二、压缩算法比较 三、压缩位置选择 四、压缩参数配置 1)为了支持多种压缩/解压缩算法,Hadoop引入了编码/解码器 2)要在Hadoop中启用压缩,可以配置如下参数

Makefile简明使用教程

文章目录 规则makefile文件的基本语法:加在命令前的特殊符号:.PHONY伪目标: Makefilev1 直观写法v2 加上中间过程v3 伪目标v4 变量 make 选项-f-n-C Make 是一种流行的构建工具,常用于将源代码转换成可执行文件或者其他形式的输出文件(如库文件、文档等)。Make 可以自动化地执行编译、链接等一系列操作。 规则 makefile文件

hdu1043(八数码问题,广搜 + hash(实现状态压缩) )

利用康拓展开将一个排列映射成一个自然数,然后就变成了普通的广搜题。 #include<iostream>#include<algorithm>#include<string>#include<stack>#include<queue>#include<map>#include<stdio.h>#include<stdlib.h>#include<ctype.h>#inclu

使用opencv优化图片(画面变清晰)

文章目录 需求影响照片清晰度的因素 实现降噪测试代码 锐化空间锐化Unsharp Masking频率域锐化对比测试 对比度增强常用算法对比测试 需求 对图像进行优化,使其看起来更清晰,同时保持尺寸不变,通常涉及到图像处理技术如锐化、降噪、对比度增强等 影响照片清晰度的因素 影响照片清晰度的因素有很多,主要可以从以下几个方面来分析 1. 拍摄设备 相机传感器:相机传

OpenHarmony鸿蒙开发( Beta5.0)无感配网详解

1、简介 无感配网是指在设备联网过程中无需输入热点相关账号信息,即可快速实现设备配网,是一种兼顾高效性、可靠性和安全性的配网方式。 2、配网原理 2.1 通信原理 手机和智能设备之间的信息传递,利用特有的NAN协议实现。利用手机和智能设备之间的WiFi 感知订阅、发布能力,实现了数字管家应用和设备之间的发现。在完成设备间的认证和响应后,即可发送相关配网数据。同时还支持与常规Sof

【C++】_list常用方法解析及模拟实现

相信自己的力量,只要对自己始终保持信心,尽自己最大努力去完成任何事,就算事情最终结果是失败了,努力了也不留遗憾。💓💓💓 目录   ✨说在前面 🍋知识点一:什么是list? •🌰1.list的定义 •🌰2.list的基本特性 •🌰3.常用接口介绍 🍋知识点二:list常用接口 •🌰1.默认成员函数 🔥构造函数(⭐) 🔥析构函数 •🌰2.list对象

【Prometheus】PromQL向量匹配实现不同标签的向量数据进行运算

✨✨ 欢迎大家来到景天科技苑✨✨ 🎈🎈 养成好习惯,先赞后看哦~🎈🎈 🏆 作者简介:景天科技苑 🏆《头衔》:大厂架构师,华为云开发者社区专家博主,阿里云开发者社区专家博主,CSDN全栈领域优质创作者,掘金优秀博主,51CTO博客专家等。 🏆《博客》:Python全栈,前后端开发,小程序开发,人工智能,js逆向,App逆向,网络系统安全,数据分析,Django,fastapi

让树莓派智能语音助手实现定时提醒功能

最初的时候是想直接在rasa 的chatbot上实现,因为rasa本身是带有remindschedule模块的。不过经过一番折腾后,忽然发现,chatbot上实现的定时,语音助手不一定会有响应。因为,我目前语音助手的代码设置了长时间无应答会结束对话,这样一来,chatbot定时提醒的触发就不会被语音助手获悉。那怎么让语音助手也具有定时提醒功能呢? 我最后选择的方法是用threading.Time