作者:胡领情

【本文正在参与51CTO HarmonyOS技术社区创作者激励计划-星光计划2.0】

1 简介

通讯录demo主要分为联系人界面、设置紧急联系人、服务卡片3个模块,分为JavaJS两个版本,本篇主要讲解用尽可能的用Java去实现。

1.1 原型

感兴趣的小伙伴,可以自己根据原型效果自己尝试着去实现【通讯录demo简易原型】

image20211207101957823.png image20211207102020088.png image20211207102034433.png

1.2 场景示例

通过学习与练习本demo,可以延伸至以下场景

城市选择.png 航班选择.png 通讯录.png

1.3 项目实战

《HarmonyOS 项目实战之通讯录Demo(JS)》 《HarmonyOS 项目实战之通讯录(Java)》 《HarmonyOS 项目实战之新闻头条(ArkUI-TS》

2 功能开发

2.1 联系人列表

2.1.1 实现效果

list.gif

2.1.2 核心代码

参考:ListContainer-常用组件开发指导-Java UI框架-UI-开发-HarmonyOS应用开发

  • ListContainer设置StickyContactProvider适配器
  • HeaderDecor头部联动效果设置
  • ContactData数据处理相关类,sortContactData方法用于排序等数据处理
        ContactData categoryData = ContactData.get();
        categoryData.sortContactData();

        contactList = (ListContainer) findComponentById(ResourceTable.Id_contactList);
        Text headerText = (Text) findComponentById(ResourceTable.Id_sticky_text);
        List<ContactBean> dataList = categoryData.getResultList();

        mStickyContactProvider = new StickyContactProvider(this, dataList);
        contactList.setItemProvider(mStickyContactProvider);
        HeaderDecor headerDecor = new HeaderDecor(contactList, headerText);

sortContactData方法数据处理,排序,字母索引:

public void sortContactData() {
    List<ContactBean> mContactList = new ArrayList<>();
    Map<String, String> map = new HashMap<>();

    for (ContactBean contactBean : mContactBeans) {
        String pinyin = Utils.getPingYin(contactBean.getName());
        map.put(pinyin, contactBean.getName());
        contactBean.setNamepy(pinyin);
        mContactList.add(contactBean);
    }
    mContactList.sort(new ContactComparator());
    characterList = new ArrayList<>();
    resultList = new ArrayList<>();
    for (ContactBean contactBean : mContactList) {
        String namepy = contactBean.getNamepy();
        String character = (namepy.charAt(0) + "").toUpperCase(Locale.ENGLISH);
        if (!characterList.contains(character)) {
            if (character.hashCode() >= "A".hashCode() && character.hashCode() <= "Z".hashCode()) { // 是字母
                characterList.add(character);
                resultList.add(new ContactBean(character, ContactBean.ITEM_TYPE.ITEM_TYPE_CHARACTER.ordinal()));
            } else {
                if (!characterList.contains("#")) {
                    characterList.add("#");
                    resultList.add(new ContactBean("#", ContactBean.ITEM_TYPE.ITEM_TYPE_CHARACTER.ordinal()));
                }
            }
        }

        resultList.add(new ContactBean(contactBean.getName(), contactBean.getTelephone(), map.get(namepy), ContactBean.ITEM_TYPE.ITEM_TYPE_CONTACT.ordinal()));
    }
}

2.2 数据的增删改查

2.2.1 实现效果

zsgc.gif

2.2.2 增删改查实现

  • ListContainer删除实现

    categoryData.deleteContactBeans(item);
    categoryData.sortContactData();
    mStickyContactProvider.setDataListChanged(categoryData.getResultList());
    
  • 随机添加一个联系人

    categoryData.addContactBean("胡六一", "15269856587");
    categoryData.sortContactData();
    mStickyContactProvider.setDataListChanged(categoryData.getResultList());
    
  • ContactData数据处理效果类,实现数据增删改查

    // Generate the javaBean of ContactData
    public static ContactData get() {
        return new ContactData();
    }
    
    public List<ContactBean> getDefaultContactBeans() {
        return mDefaultContactBeans;
    }
    
    public List<ContactBean> getContactBeans() {
        return mContactBeans;
    }
    
    public void addContactBean(String name, String phone) {
        mContactBeans.add(new ContactBean(name, phone));
    }
    
    public List<ContactBean> deleteContactBeans(ContactBean item) {
        mContactBeans.removeIf(contactBean -> contactBean.getName().equals(item.getName()));
    
        return mContactBeans;
    }
    
    public List<ContactBean> getResultList() {
        return resultList;
    }
    
    public List<String> getCharacterList() {
        return characterList;
    }
    
    public int getScrollPosition(String character) {
        if (characterList.contains(character)) {
            for (int i = 0; i < resultList.size(); i++) {
                if (resultList.get(i).getCharacter().equals(character)) {
                    return i;
                }
            }
        }
    
        return -1; // -1不会滑动
    }
    

2.2.3 紧急联系人数据存储

轻量级数据存储轻量级数据存储概述-轻量级数据存储-数据管理-开发-HarmonyOS应用开发

  • Key-Value数据结构

    一种键值结构数据类型。Key是不重复的关键字,Value是数据值。

运作机制

  • 本模块提供轻量级数据存储的操作类,应用通过这些操作类完成数据库操作。
  • 借助DatabaseHelper API,应用可以将指定文件的内容加载到Preferences实例,每个文件最多有一个Preferences实例,系统会通过静态容器将该实例存储在内存中,直到应用主动从内存中移除该实例或者删除该文件。
  • 获取到文件对应的Preferences实例后,应用可以借助Preferences API,从Preferences实例中读取数据或者将数据写入Preferences实例,通过flush或者flushSync将Preferences实例持久化。

核心代码实现

  • 添加紧急联系人,并通知java卡片更新

    ZSONObject zsonObject = new ZSONObject();
    zsonObject.put("urgent1", nameTf1.getText());
    zsonObject.put("urgentPhone1", phoneTf1.getText());
    zsonObject.put("urgent2", nameTf2.getText());
    zsonObject.put("urgentPhone2", phoneTf2.getText());
    PreferenceUtils.putString(getContext(),"urgentPerson", ZSONObject.toZSONString(zsonObject));
    FormBindingData formBindingData = new FormBindingData(zsonObject);
    ((ContactPersonAbility) getAbility()).confirmUpdateForm(formBindingData);
    
  • PreferenceUtils封装工具类,实现数据存储

    public class PreferenceUtils {
        private static String PREFERENCE_FILE_NAME = "prefrence_file";
        private static Preferences preferences;
        private static DatabaseHelper databaseHelper;
        private static Preferences.PreferencesObserver mPreferencesObserver;
    
        private static void initPreference(Context context) {
            if (databaseHelper == null) {
                databaseHelper = new DatabaseHelper(context);
            }
            if (preferences == null) {
                preferences = databaseHelper.getPreferences(PREFERENCE_FILE_NAME);
            }
        }
    
        //存放、获取时传入的context必须是同一个context,否则存入的数据无法获取
        public static void putString(Context context, String key, String value) {
            initPreference(context);
            preferences.putString(key, value);
            preferences.flush();
        }
    
        /**
         * @param context 上下文
         * @param key     键
         * @return 获取的String 默认值为:null
         */
        public static String getString(Context context, String key) {
            initPreference(context);
            return preferences.getString(key, null);
        }
    
        public static String getString(Context context, String key, String d) {
            initPreference(context);
            return preferences.getString(key, d);
        }
    
        public static void putInt(Context context, String key, int value) {
            initPreference(context);
            preferences.putInt(key, value);
            preferences.flush();
        }
    
        /**
         * @param context 上下文
         * @param key     键
         * @return 获取int的默认值为:-1
         */
        public static int getInt(Context context, String key) {
            initPreference(context);
            return preferences.getInt(key, -1);
        }
        public static void putLong(Context context, String key, long value) {
            initPreference(context);
            preferences.putLong(key, value);
            preferences.flush();
        }
    
        /**
         * @param context 上下文
         * @param key     键
         * @return 获取long的默认值为:-1
         */
        public static long getLong(Context context, String key) {
            initPreference(context);
            return preferences.getLong(key, -1L);
        }
    
        public static void putBoolean(Context context, String key, boolean value) {
            initPreference(context);
            preferences.putBoolean(key, value);
            preferences.flush();
        }
    
        /**
         * @param context 上下文
         * @param key     键
         * @return 获取boolean的默认值为:false
         */
        public static boolean getBoolean(Context context, String key) {
            initPreference(context);
            return preferences.getBoolean(key, false);
        }
    
        public static void putFloat(Context context, String key, float value) {
            initPreference(context);
            preferences.putFloat(key, value);
            preferences.flush();
        }
    
        /**
         * @param context 上下文
         * @param key     键
         * @return 获取float的默认值为:0.0
         */
        public static float getFloat(Context context, String key) {
            initPreference(context);
            return preferences.getFloat(key, 0.0F);
        }
    
        public static void putStringSet(Context context, String key, Set<String> set) {
            initPreference(context);
            preferences.putStringSet(key, set);
            preferences.flush();
        }
    
        /**
         * @param context 上下文
         * @param key     键
         * @return 获取set集合的默认值为:null
         */
        public static Set<String> getStringSet(Context context, String key) {
            initPreference(context);
            return preferences.getStringSet(key, null);
        }
    
        public static boolean deletePreferences(Context context) {
            initPreference(context);
            boolean isDelete = databaseHelper.deletePreferences(PREFERENCE_FILE_NAME);
            return isDelete;
        }
    
        public static void registerObserver(Context context, Preferences.PreferencesObserver preferencesObserver) {
            initPreference(context);
            mPreferencesObserver = preferencesObserver;
            preferences.registerObserver(mPreferencesObserver);
        }
    
        public static void unregisterObserver() {
            if (mPreferencesObserver != null) {
                // 向preferences实例注销观察者
                preferences.unregisterObserver(mPreferencesObserver);
            }
        }
    

2.3 第三方跳转

2.3.1 实现效果

call.gif

2.3.2 拨打电话与发送短信

/**
     * 跳转系统短信
     */
    private void doMessage(String telephone) {
        Intent intent = new Intent();
        Operation operation = new Intent.OperationBuilder()
//                .withAction("android.intent.action.SENDTO") // Android写法 android.intent.action.SENDTO
                .withAction(IntentConstants.ACTION_SEND_SMS)
                .withUri(Uri.parse("smsto:" + telephone)) // 设置号码
                .withFlags(Intent.FLAG_NOT_OHOS_COMPONENT)
                .build();
        intent.setOperation(operation);
        context.startAbility(intent, 11);
    }

    /**
     * 申请拨打电话权限
     */
    private boolean requestPermissions() {
        if (context.verifySelfPermission("android.permission.CALL_PHONE") != IBundleManager.PERMISSION_GRANTED) {
            // 应用未被授予权限
            if (context.canRequestPermission("android.permission.CALL_PHONE")) {
                // 是否可以申请弹框授权(首次申请或者用户未选择禁止且不再提示)
                context.requestPermissionsFromUser(new String[]{"android.permission.CALL_PHONE"}, 100);
            }
            return false;
        } else {
            // 权限已被授予
            return true;
        }
    }

    /**
     * 直接拨打电话
     * 需要申请权限
     */
    private void doCall(String destinationNum) {
        if (!requestPermissions()) {
            return;
        }
        Intent intent = new Intent();
        Operation operation = new Intent.OperationBuilder()
                .withAction("android.intent.action.CALL") // 系统应用拨号盘
                .withUri(Uri.parse("tel:" + destinationNum)) // 设置号码
                .withFlags(2)
                .build();
        intent.setOperation(operation);
        // 启动Ability
        context.startAbility(intent, 10);

    }

    /**
     * 跳转系统拨打电话界面
     */
    private void doDial(String destinationNum) {
        Intent intent = new Intent();
        Operation operation = new Intent.OperationBuilder()
                .withAction(IntentConstants.ACTION_DIAL) // 系统应用拨号盘
//                .withBundleName(context.getCallingBundle()) // 应用拨号选择器
                .withUri(Uri.parse("tel:" + destinationNum)) // 设置号码
                .withFlags(2)
                .build();
        intent.setOperation(operation);
        // 启动Ability
        context.startAbility(intent, 10);

    }

2.4 JS服务卡片

2.4.1 实现效果

kapian.gif

2.4.2 创建卡片模板

  • 使用DevEco Studio创建卡片工程

服务卡片新建

  • 创建成功后,在config.json的module中会生成js模块,用于对应卡片的js相关资源,配置示例如下:

    "js": [
      {
        "pages": [
          "pages/index/index"
        ],
        "name": "widget",
        "window": {
          "designWidth": 720,
          "autoDesignWidth": true
        },
        "type": "form"
      }
    ]
    
  • config.json文件“abilities”配置forms模块细节如下:

    "name": "com.huhu.contact.ContactPersonAbility",
    "icon": "$media:icon",
    "description": "$string:contactpersonability_description",
    "formsEnabled": true,
    "label": "$string:contact_ContactPersonAbility",
    "type": "page",
    "forms": [
      {
        "jsComponentName": "widget",
        "isDefault": true,
        "scheduledUpdateTime": "10:30",
        "defaultDimension": "2*2",
        "name": "widget",
        "description": "This is a service widget",
        "colorMode": "auto",
        "type": "JS",
        "supportDimensions": [
          "2*2"
        ],
        "updateEnabled": true,
        "updateDuration": 1
      }
    ]
    
  • 创建一个ContactPersonAbility,覆写卡片相关回调函数。

    • onCreateForm(Intent intent)
    • onUpdateForm(long formId)
    • onDeleteForm(long formId)
    • onCastTempForm(long formId)
    • onEventNotify(Map<Long, Integer> formEvents)
    • onTriggerFormEvent(long formId, String message)
    • onAcquireFormState(Intent intent)

    当卡片使用方请求获取卡片时,卡片提供方会被拉起并调用onCreateForm(Intent intent)回调,intent中会带有卡片ID、卡片名称和卡片外观规格信息,可按需获取使用。

    开发JS卡片时,FormAbility可以继承AceAbility或Ability,继承Ability时,需在onStart()方法中额外设置路由信息。

2.4.3 卡片数据绑定

    @Override
    public ProviderFormInfo bindFormData() {
        HiLog.info(TAG, "bind form data");
        ProviderFormInfo providerFormInfo = new ProviderFormInfo();
        String urgentPersonStr = PreferenceUtils.getString(context, "urgentPerson", "");
        ZSONObject zsonObject = ZSONObject.stringToZSON(urgentPersonStr);
        if (dimension == DEFAULT_DIMENSION_2X2) {
            if (zsonObject != null) {
                providerFormInfo.setJsBindingData(new FormBindingData(zsonObject));
            }
        }
        return providerFormInfo;
    }

2.4.4 卡片数据更新

public void confirmUpdateForm(FormBindingData formBindingData) {
    FormControllerManager formControllerManager = FormControllerManager.getInstance(this);
    List<Long> allFormIdFromSharePreference = formControllerManager.getAllFormIdFromSharePreference();
    if (allFormIdFromSharePreference == null || allFormIdFromSharePreference.isEmpty()) return;
    Long formId = allFormIdFromSharePreference.get(0);
    try {
        updateForm(formId,formBindingData);
    } catch (FormException e) {
        e.printStackTrace();
    }

2.4.5 卡片事件处理

{
  "data": {
    "text_content": "Name",
    "cardPrimaryText": "Contacts",
    "cardSecondaryText": "+8612345678912",
    "urgent1": "无",
    "urgent2": "无",
    "urgentPhone1": "+8612345678912",
    "urgentPhone2": "+8612345678915"
  },
  "actions": {
    "urgentCall1": {
      "action": "message",
      "params": {
        "action": "urgentCall1",
        "phoneNumber": "10086"
      }
    },
    "urgentCall2": {
      "action": "message",
      "params": {
        "action": "urgentCall2",
        "phoneNumber": "15565339857"
      }
    },
    "startMainRouter": {
      "action": "router",
      "abilityName": "com.huhu.contact.ContactPersonAbility"
    }
  }
}

卡片支持触发事件,覆写onTriggerFormEvent方法实现对事件的触发,

doCall就是前面的播打电话的方法

    @Override
    protected void onTriggerFormEvent(long formId, String message) {
        super.onTriggerFormEvent(formId, message);
        HiLog.info(loglabel, "onTriggerFormEvent: " + message);
        FormControllerManager formControllerManager = FormControllerManager.getInstance(this);
        FormController formController = formControllerManager.getController(formId);
        formController.onTriggerFormEvent(formId, message);
        ZSONObject params = ZSONObject.stringToZSON(message);
        String action = params.getString("action");
        String phoneNumber = params.getString("phoneNumber");
        HiLog.info(loglabel, "onTriggerFormEvent: action:" + action);

        String urgentPersonStr = PreferenceUtils.getString(this, "urgentPerson", "");
        ZSONObject zsonObject = ZSONObject.stringToZSON(urgentPersonStr);
        switch (action) {
            case "urgentCall1":
                String urgentPhone1 = zsonObject.getString("urgentPhone1");
                doCall(urgentPhone1);
                break;
            case "urgentCall2":
                String urgentPhone2 = zsonObject.getString("urgentPhone2");
                doCall(urgentPhone2);
                break;
            default:
                break;
        }
    }

3 注意事项

Demo还有很多需要完善的地方

  1. 滑动时,上滑头部联动效果,索引有时会错乱;

  2. 紧急联系人没有和列表数据联动。

  3. 搜索功能未实现;

4 总结

有不对或者更优的处理技术方案请多多指教,共同学习,共同进步。

代码地址: https://gitee.com/hu-lingqing/contact-person.git

更多原创内容请关注:深开鸿技术团队

入门到精通、技巧到案例,系统化分享HarmonyOS开发技术,欢迎投稿和订阅,让我们一起携手前行共建鸿蒙生态。

想了解更多关于鸿蒙的内容,请访问:

51CTO和华为官方合作共建的鸿蒙技术社区

https://harmonyos.51cto.com/#bkwz

::: hljs-center

21_9.jpg

:::