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

前言

看了很久的鸿蒙开发文档,对鸿蒙开发有了一定的了解;那么就手动实现一个鸿蒙客户端,作为对文档知识的整合,主要使用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");

    @Override
    public 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 {
    @Override
    public Response intercept(Chain chain) throws IOException {
        Response originalResponse = chain.proceed(chain.request());
        //这里获取请求返回的cookie
        if (!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 {
    @Override
    public 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();

    @Override
    protected 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();

    @Override
    protected Component onComponentAttached(LayoutScatter scatter, ComponentContainer container, Intent intent) {
        mComponent = scatter.parse(getUIContent(), container, false);
        initComponent();
        return mComponent;
    }

    @Override
    protected void onStart(Intent intent) {
        super.onStart(intent);
    }

    @Override
    protected void onActive() {
        super.onActive();
    }

    @Override
    protected void onForeground(Intent intent) {
        super.onForeground(intent);
    }

    @Override
    protected void onInactive() {
        super.onInactive();

    }

    @Override
    protected 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;

    @Override
    protected void onStart(Intent intent) {
        super.onStart(intent);
    }

    @Override
    protected void onStop() {
        if (mPresenter != null){
            mPresenter.unBindView();
        }
        super.onStop();
    }
}  

BaseMVPFraction:

public abstract class BaseMVPFraction <T extends BasePresenter> extends BaseFraction implements BaseView {

    protected T mPresenter;

    @Override
    protected 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{
    @Override
    void showLoading();

    @Override
    void hideLoading();

    @Override
    void 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{
        @Override
        void showLoading();

        @Override
        void hideLoading();

        @Override
        void 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 {
    @Override
    public Call<BaseResult<List<BannerModule>>> getHomeBanner() {
        return RetrofitUtil.getInstance().getService().getBanner();
    }

    @Override
    public 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();
    }

    @Override
    public void getHomeBanner() {
        if (!isBindView())
            return;

        mView.showLoading();
        //通过model指定方法进行请求
        homeModel.getHomeBanner().enqueue(new Callback<BaseResult<List<BannerModule>>>() {
            @Override
            public void onResponse(Call<BaseResult<List<BannerModule>>> call, Response<BaseResult<List<BannerModule>>> response) {
                mView.hideLoading();
                if (response.body().getErrorCode() == 0){
                    //请求成功将数据回调到成功的方法
                    mView.onBannerSuccess(response.body().getData());
                }
            }

            @Override
            public void onFailure(Call<BaseResult<List<BannerModule>>> call, Throwable throwable) {
                mView.hideLoading();
                //请求失败将错误信息回调到失败的方法
                mView.onFailure(throwable.getMessage());
            }
        });
    }

    @Override
    public void getArticleList(int page) {
        if (!isBindView())
            return;

        mView.showLoading();
        homeModel.getArticleList(page).enqueue(new Callback<BaseResult<ArticleModule>>() {
            @Override
            public void onResponse(Call<BaseResult<ArticleModule>> call, Response<BaseResult<ArticleModule>> response) {
                mView.hideLoading();
                if (response.body().getErrorCode() == 0){
                    mView.onArticleSuccess(response.body().getData());
                }
            }

            @Override
            public 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;
  
      @Override
      public int getUIContent() {
          return ResourceTable.Layout_fraction_home;
      }
  
      @Override
      public 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);
      }
  
      @Override
      public void showLoading() {
  
      }
  
      @Override
      public void hideLoading() {
  
      }
  
      @Override
      public void onFailure(String errorMsg) {
  
      }
  
      @Override
      public void onBannerSuccess(List<BannerModule> bannerList) {
      }
  
      @Override
      public 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() {
      @Override
      public void onPageSliding(int i, float v, int i1) {

      }

      @Override
      public void onPageSlideStateChanged(int i) {

      }

      @Override
      public 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 {
 
    @Override
    public void onStart(Intent intent) {
        super.onStart(intent);
        super.setMainRoute(TestFractionAbilitySlice.class.getName());
    }
}

创建布局文件使用StackLayout:

<?xml version="1.0" encoding="utf-8"?>
<DirectionalLayout
    xmlns: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">
 
    <StackLayout
        ohos:id="$+id:test_fraction"
        ohos:width="match_parent"
        ohos:height="match_parent"/>
 
</DirectionalLayout>

创建Fraction的布局文件:

<?xml version="1.0" encoding="utf-8"?>
<DirectionalLayout
    xmlns: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">
 
    <Text
        ohos: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 {
    @Override
    protected Component onComponentAttached(LayoutScatter scatter, ComponentContainer container, Intent intent) {
        Component component = scatter.parse(ResourceTable.Layout_test_fraction, container, false);
        return component;
    }
 
    @Override
    protected 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 {
 
    @Override
    protected 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,并设置相关属性:

<TabList
    ohos: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() {
      @Override
      public void onSelected(TabList.Tab tab) {
           pageSlider.setCurrentPage(tab.getPosition());//关联pageSlider
      }

      @Override
      public void onUnselected(TabList.Tab tab) {

      }

      @Override
      public void onReselected(TabList.Tab tab) {

      }
});

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

PageSlider的使用

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

xml布局创建:

<PageSlider
    ohos: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;
    }

    @Override
    public int getCount() {
        return dataList.size();
    }

    @Override
    public 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;
    }

    @Override
    public void destroyPageFromContainer(ComponentContainer componentContainer, int i, Object o) {
        componentContainer.removeComponent((Component) o);
    }

    @Override
    public boolean isPageMatchToObject(Component component, Object o) {
        return component == o;
    }
}

绑定Provider:

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

监听滑动,并绑定Tablist:

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

在onPageChosen方法关联TabList。

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

Glide在鸿蒙OS中的使用

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

1.build.gradle添加maven:

allprojects {
   repositories {
       //打开maven
       mavenCentral()
       //.....
   }
}

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.FlowLayout
       ohos: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"?>
<DirectionalLayout
    xmlns: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.ToolbarLayout
        ohos: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"?>
  <DirectionalLayout
      xmlns: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">
  
      <WebView
          ohos:id="$+id:webview"
          ohos:height="match_parent"
          ohos:width="match_parent"/>
  
  </DirectionalLayout>

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

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

<?xml version="1.0" encoding="utf-8"?>
  <DirectionalLayout
      xmlns: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.WebView
          ohos: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 = "";
    @Override
    public 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);
    }

    @Override
    public void onActive() {
        super.onActive();
    }

    @Override
    public void onForeground(Intent intent) {
        super.onForeground(intent);
    }

    private void initWebView(String link){
        webView.getWebConfig().setJavaScriptPermit(true);//支持js
        webView.load(link);
    }

    @Override
    public 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{
    @Override
    public void onChange(ChangeNotification changeNotification) {
        System.out.println("发生变化,onChange被回调");
        //新增的数据
        List<Entry> insertEntries = changeNotification.getInsertEntries();
        //发生改变的数据
        List<Entry> updateEntries = changeNotification.getUpdateEntries();
        //删除的数据
        List<Entry> deleteEntries = changeNotification.getDeleteEntries();
        //发生变化的设备id
        String 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。
戳我戳我~

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值