Android通讯录功能实现基础教程

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:在Android开发中,实现通讯录功能对于管理用户个人信息及通信至关重要。本教程通过一个基础的Demo项目,详细指导初学者如何在Android平台上从获取数据到界面展示完整实现通讯录功能。涵盖内容包括使用Content Provider和ContactsContract类操作联系人数据,获取必要的权限,查询联系人信息,以及展示数据。同时,讲解了如何处理用户交互和在不同Android版本上适配联系人API。项目文件"AndroidContact_beta9"提供了一个完整的项目源码参考,帮助开发者深入理解通讯录功能的实现原理,并能在实际开发中应用。

1. Android通讯录功能概述

在现代移动应用中,通讯录功能是一个不可或缺的部分,它让用户能够管理和访问他们的联系人信息。本章将简要介绍Android通讯录功能的基础概念,并概述其在移动设备中扮演的角色。我们将讨论通讯录数据存储的基本原理,以及它与操作系统和第三方应用的交互方式。

1.1 通讯录功能的重要性

通讯录不仅存储联系人信息,还提供搜索、编辑和删除等操作,是移动应用与用户日常交互的重要组件。对于开发者而言,高效且用户友好的通讯录功能可以提升整体用户体验。

1.2 Android通讯录数据模型

在Android平台上,通讯录数据由系统级数据库管理,开发者通过 ContactsContract API与之交互。这允许应用读取和修改联系人数据,但需要相应的权限。

1.3 开发通讯录功能的基本步骤

实现通讯录功能需要了解 ContactsContract API的使用方法,处理Android权限系统,并且掌握数据管理与用户界面展示的技术细节。

通过本章的学习,您将对Android通讯录有一个初步的理解,为后续章节中更深入的学习打下基础。

2. 实现Android通讯录的基础操作

2.1 联系人API和ContactsContract类使用

2.1.1 ContactsContract类的基本介绍

在Android开发中,通讯录的应用主要依赖于 ContactsContract 类。这是一个内容提供者类,它允许应用程序访问设备上的联系人信息。 ContactsContract 提供了丰富的API,允许开发者读取、写入、搜索、查询和操作用户设备中的联系人数据。

ContactsContract 下分为几个主要部分: - ContactsContract.Contacts :代表单个联系人信息。 - ContactsContract.CommonDataKinds.Phone :联系人的电话信息。 - ContactsContract.CommonDataKinds.Email :联系人的电子邮箱信息。 - ContactsContract.CommonDataKinds.StructuredName :联系人的结构化姓名信息。

使用 ContactsContract 类时,您通常会与 ContentResolver 进行交云,该类提供了访问和操作数据的统一接口。每个操作都会使用一个特定的URI,例如 ContactsContract.Contacts.CONTENT_URI 用于查询所有联系人。

2.1.2 常用的联系人查询方法

在进行联系人查询时,以下是一些常用的方法:

  • query() : 执行查询操作。需要指定URI、projection(查询列)、selection(过滤条件)、selectionArgs(参数)、sortOrder(排序方式)。
  • insert() : 插入新的联系人数据。
  • update() : 更新现有联系人信息。
  • delete() : 删除指定的联系人数据。
// 示例:查询所有联系人的名称和电话号码
ContentResolver cr = getContentResolver();
String[] projection = {
    ContactsContract.Contacts.DISPLAY_NAME,
    ContactsContract.CommonDataKinds.Phone.NUMBER
};
Cursor cursor = cr.query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
                         projection,
                         null,
                         null,
                         null);

在上述代码中,我们创建了一个 Cursor 对象,它指向了一个查询结果集。然后可以根据 Cursor 进行遍历,获取每个联系人的详细信息。

2.2 权限获取(READ_CONTACTS 和 WRITE_CONTACTS)

2.2.1 Android权限系统概述

Android的安全模型中,权限是保护用户数据和系统安全的关键。权限分为两类:普通权限和危险权限。普通权限对用户的安全和设备正常运行的影响较小,而危险权限则涉及到用户隐私或者设备的敏感功能,需要用户明确授权。

在操作通讯录时,至少需要以下两个危险权限: - READ_CONTACTS :读取联系人的权限。 - WRITE_CONTACTS :写入联系人的权限。

如果应用目标API级别是Android 6.0(API级别23)或者更高,那么在运行时请求权限成为必须。

2.2.2 申请和检查READ_CONTACTS和WRITE_CONTACTS权限

在Android 6.0及以上版本,应用需要在运行时请求权限。以下是一个示例代码,展示如何在运行时请求权限:

// 检查并请求权限
private void checkAndRequestPermission() {
    // 检查权限是否已经被授予
    if (ContextCompat.checkSelfPermission(thisActivity, Manifest.permission.READ_CONTACTS)
            != PackageManager.PERMISSION_GRANTED) {
        // 如果没有被授予,则请求权限
        ActivityCompat.requestPermissions(thisActivity,
                new String[]{Manifest.permission.READ_CONTACTS}, REQUEST_CODE);
    } else {
        // 已经授予,可以进行操作...
    }
}

// 处理权限请求结果
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
    super.onRequestPermissionsResult(requestCode, permissions, grantResults);
    if (requestCode == REQUEST_CODE) {
        if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
            // 权限被授予,继续操作...
        } else {
            // 权限被拒绝,进行相应处理...
        }
    }
}

在上述代码中, REQUEST_CODE 是一个自定义整数,用于识别权限请求。如果权限被用户拒绝,应用应当给出相应的提示或者提供替代方案。

通过上述步骤,应用将能够确保拥有操作通讯录所需的权限,从而在保证用户隐私的同时,实现对通讯录数据的有效管理。

3. 通讯录数据管理与界面展示

通讯录的主要作用是存储和展示用户的联系信息,因此数据管理和界面展示成为了实现这一功能的核心。在Android平台上,数据管理是通过ContentProvider完成的,而界面展示则涉及到了使用ListView或RecyclerView等UI组件。本章节将详细探讨如何查询、展示联系人数据,以及如何通过自定义Adapter将数据映射到界面组件。

3.1 联系人数据查询与展示

3.1.1 使用ContentResolver查询联系人数据

ContentResolver是Android提供的一套标准API,用于访问和修改设备上的数据,例如联系人、通话记录等。它是ContentProvider接口的实现,允许应用程序对数据进行增删改查操作。对于通讯录功能而言,我们主要使用ContentResolver查询数据。

查询联系人时,通常会使用到 query() 方法。此方法允许我们指定要查询的URI(通用资源标识符)、需要返回的列、查询条件、排序方式以及返回的游标类型。以下是一个简单的示例代码,展示了如何使用ContentResolver来查询通讯录中的所有联系人:

Cursor cursor = getContentResolver().query(
    ContactsContract.CommonDataKinds.Phone.CONTENT_URI, 
    new String[] { 
        ContactsContract.CommonDataKinds.Phone._ID, 
        ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME, 
        ContactsContract.CommonDataKinds.Phone.NUMBER 
    }, 
    null, 
    null, 
    null
);

在这个查询中,我们指定了要从 ContactsContract.CommonDataKinds.Phone.CONTENT_URI 查询,返回的数据列包括联系人的ID、显示名称和电话号码。由于这里没有特别指定查询条件,所以返回的是通讯录中所有联系人的信息。

查询完成后,你将获得一个 Cursor 对象,可以通过 moveToNext() 方法遍历整个结果集,并使用 getString() getInt() 等方法根据列索引或列名获取相应的数据。

3.1.2 使用ListView或RecyclerView展示数据

在Android应用中,数据展示通常使用ListView或RecyclerView。对于数据量较少的通讯录,ListView足矣;但对于数据量较大或需要更复杂布局的场景,使用RecyclerView则更为合适。

ListView展示数据的基本步骤如下:

  1. 创建一个布局文件,其中包含ListView组件。
  2. 在Activity或Fragment中,通过findViewById()方法获取ListView实例。
  3. 创建一个ArrayAdapter(或自定义Adapter),并将其与ListView关联。
  4. 使用ArrayAdapter的 add() 方法添加数据项,此时每个数据项代表一个联系人。
  5. 将ArrayAdapter实例设置为ListView的适配器。

示例代码:

// 假设已有Cursor cursor包含联系人数据
ArrayAdapter<String> adapter = new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, cursor);
ListView listView = findViewById(R.id.myListView);
listView.setAdapter(adapter);

这里,我们使用了Android系统提供的 simple_list_item_1 布局来简单展示联系人名称。对于更复杂的展示,我们可以创建自定义的布局文件,并通过自定义Adapter将Cursor中的数据绑定到对应的UI元素上。

RecyclerView展示数据的流程与ListView类似,但提供了更灵活的布局管理和性能优化机制。它要求开发者实现 RecyclerView.Adapter RecyclerView.LayoutManager ,这为实现复杂列表布局提供了更大的自由度。

3.2 自定义Adapter实现数据到视图的映射

3.2.1 创建自定义Adapter类

当内置的Adapter无法满足特定需求时,我们可以自定义Adapter。自定义Adapter允许我们精确控制如何将数据映射到视图。以下是一个简单的自定义Adapter的示例代码:

public class CustomAdapter extends RecyclerView.Adapter<CustomAdapter.ViewHolder> {
    private Cursor cursor;

    public CustomAdapter(Cursor cursor) {
        this.cursor = cursor;
    }

    public static class ViewHolder extends RecyclerView.ViewHolder {
        public TextView textView;
        public ViewHolder(View view) {
            super(view);
            textView = (TextView) view.findViewById(R.id.textView);
        }
    }

    @Override
    public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.custom_item, parent, false);
        return new ViewHolder(v);
    }

    @Override
    public void onBindViewHolder(ViewHolder holder, int position) {
        cursor.moveToPosition(position);
        String name = cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME));
        holder.textView.setText(name);
    }

    @Override
    public int getItemCount() {
        return cursor.getCount();
    }
}

这个自定义Adapter适用于RecyclerView,其中 onCreateViewHolder 方法负责创建视图, onBindViewHolder 方法负责将数据绑定到视图上。

3.2.2 绑定数据和视图

绑定数据和视图是通过 onBindViewHolder 方法实现的。在本示例中,我们将从Cursor中获取的联系人姓名显示在TextView中。绑定逻辑如下:

  • onBindViewHolder 方法中,使用 moveToPosition 方法移动Cursor到当前行。
  • 使用 getColumnIndex 获取数据列的索引。
  • 使用 getString 方法根据索引获取联系人姓名。
  • 将获取的联系人姓名设置到TextView上。

实现数据到视图的绑定后,就可以在RecyclerView上展示定制化的联系人列表了。

在展示通讯录数据时,开发者需要根据具体的应用场景和性能要求来选择合适的展示组件。无论是ListView还是RecyclerView,都需结合Adapter来展示数据。自定义Adapter提供了更高的灵活性,可以针对不同的数据类型和布局需求进行优化。而在数据展示过程中,界面友好性和用户体验也是设计时应考虑的重要因素。

4. 联系人编辑与管理功能实现

4.1 新联系人创建与现有联系人编辑

4.1.1 创建联系人的基本步骤

在Android平台上,创建一个新的联系人涉及到向 ContactsContract 提供的内容提供者中插入数据。以下是创建联系人的基本步骤:

  1. 获取 ContentResolver 实例以进行操作。
  2. 创建一个 ContentValues 对象来存储联系人信息。
  3. 利用 ContentResolver insert 方法,将新的联系人信息插入到系统中。

代码块如下:

// 获取ContentResolver实例
ContentResolver resolver = getContentResolver();

// 创建一个ContentValues对象
ContentValues values = new ContentValues();
values.put(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE);
values.put(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME, "John Doe");

// 插入新的联系人信息
Uri newContactUri = resolver.insert(ContactsContract.Data.CONTENT_URI, values);

在上述代码中,首先获取了 ContentResolver 对象,它是应用程序与内容提供者之间进行数据交互的桥梁。然后创建了一个 ContentValues 对象,其中包含了要插入的数据,这里是以添加姓名为例。最后,通过 insert 方法,将新的联系人信息插入到了内容提供者中。

4.1.2 编辑现有联系人信息

编辑现有联系人信息的步骤与创建新联系人类似,但需要先查询到要编辑的联系人的ID,然后对相应的字段进行更新。以下是编辑现有联系人的基本步骤:

  1. 查询现有联系人的信息以获取其ID。
  2. 创建一个 ContentValues 对象来存储更新后的信息。
  3. 利用 ContentResolver update 方法更新联系人信息。

代码块示例如下:

// 查询现有联系人信息以获取其ID
Cursor cursor = resolver.query(ContactsContract.Contacts.CONTENT_URI, null,
                               ContactsContract.Contacts.DISPLAY_NAME + " = ?", new String[] {"John Doe"}, null);
if (cursor != null && cursor.moveToFirst()) {
    // 获取联系人的ID
    long contactId = cursor.getLong(cursor.getColumnIndex(ContactsContract.Contacts._ID));

    // 创建一个ContentValues对象
    ContentValues values = new ContentValues();
    values.put(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE);
    values.put(ContactsContract.CommonDataKinds.Phone.NUMBER, "1234567890");

    // 更新联系人信息
    Uri updateUri = resolver.update(
        ContactsContract.Data.CONTENT_URI,
        values,
        ContactsContract.Data.CONTACT_ID + " = ? AND " +
        ContactsContract.Data.MIMETYPE + " = ?", new String[] {
            String.valueOf(contactId),
            ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE
        });

    cursor.close();
}

在此代码段中,首先通过查询操作获取到指定名称的联系人ID。然后创建了一个 ContentValues 对象,并设置了要更新的数据。最后,使用 update 方法实现了更新操作。需要注意的是,在更新操作中,必须指定要更新的联系人的ID以及其他必要条件,以确保只更新目标联系人的信息。

编辑联系人的过程中,务必注意权限的申请和检查,确保应用具有修改联系人的权限( WRITE_CONTACTS )。此外,操作之前进行数据的备份,以防止数据丢失或误操作带来的问题。

4.2 联系人删除功能实现

4.2.1 删除联系人的流程

联系人的删除操作是指将联系人的信息从通讯录中移除。这一过程需要谨慎处理,因为一旦执行,与该联系人相关的所有信息都将被删除。以下是删除联系人的基本流程:

  1. 确认用户意图删除联系人。
  2. 获取联系人的ID。
  3. 使用 ContentResolver delete 方法根据联系人的ID来删除信息。

代码块示例如下:

// 确认用户意图删除联系人
// 这里使用一个对话框来确认用户意图,此处代码省略...

// 获取联系人的ID
long contactId = ...; // 从某处获取联系人ID

// 使用ContentResolver的delete方法删除信息
int deletedRows = resolver.delete(ContactsContract.Contacts.CONTENT_URI,
                                  ContactsContract.Contacts._ID + " = ?", new String[]{String.valueOf(contactId)});

在该代码块中,首先通过某种方式确认了用户删除联系人的意图。然后,通过 delete 方法根据联系人的ID来执行删除操作。该方法会返回被删除的行数,如果返回值大于0,则表示删除成功。

4.2.2 确认删除和撤销机制

在实际应用中,删除操作通常是不可逆的,因此提供一个确认删除的步骤是非常必要的。同时,为了防止误操作,一个撤销机制也是增加用户体验的重要部分。

确认删除

为了确保用户真正意图删除联系人,可以通过弹出一个对话框(例如使用 AlertDialog )来让用户再次确认。

// 使用AlertDialog确认用户删除意图
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle("确认删除");
builder.setMessage("确定要删除该联系人吗?");
builder.setPositiveButton("确认", new DialogInterface.OnClickListener() {
    @Override
    public void onClick(DialogInterface dialog, int which) {
        // 执行删除操作
    }
});
builder.setNegativeButton("取消", null);
builder.show();
撤销机制

撤销机制的实现通常需要借助于操作的历史记录或临时存储。一种简单的方法是,在删除操作执行前,将要删除的数据暂存到一个列表中,在用户选择撤销时,从列表中恢复数据。

List<Long> deletedContactIds = new ArrayList<>(); // 存储已删除联系人的ID

// 删除操作之前
deletedContactIds.add(contactId);

// 执行删除操作...

// 如果用户选择撤销
if (deletedContactIds.contains(contactId)) {
    // 将联系人信息重新插入到通讯录中
    // 此处代码省略...
}

请注意,实际撤销操作较为复杂,需要考虑的数据恢复包括但不限于:姓名、电话号码、电子邮件等。因此,撤销机制的实现需要根据应用的具体需求来详细设计。

5. 用户交互与版本兼容性优化

5.1 用户交互事件处理

用户交互是构建一个良好用户体验的应用的关键,合理的事件处理机制可以让用户感到操作的顺畅和应用的智能。

5.1.1 按钮和列表项的点击事件监听

在Android开发中,监听按钮和列表项的点击事件是常见的交互操作。以下是一个示例,展示了如何为一个按钮设置点击事件监听,并在点击后弹出一个Toast消息。

Button addButton = findViewById(R.id.add_contact_button);
addButton.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        // 当按钮被点击时,将执行以下代码
        Toast.makeText(getApplicationContext(), "添加联系人功能已被触发", Toast.LENGTH_SHORT).show();
    }
});

列表项的点击事件监听同样重要,通常我们会使用一个AdapterView来展示数据,然后通过设置OnItemClickListener来处理用户的点击事件。

ListView contactListView = findViewById(R.id.contact_list_view);
contactListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
    @Override
    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
        // position是用户点击项的位置
        Toast.makeText(getApplicationContext(), "您点击了第 " + position + " 项", Toast.LENGTH_SHORT).show();
    }
});

5.1.2 弹出菜单和上下文操作栏的实现

弹出菜单(Popup Menu)和上下文操作栏(Action Bar)是提升用户交互体验的两种方式。以下是实现这两种功能的基本方法。

弹出菜单通常用于提供额外的操作选项,可以通过定义XML菜单资源文件,然后在代码中加载此菜单。

<!-- res/menu/context_menu.xml -->
<menu xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:id="@+id/menu_edit"
          android:title="编辑"/>
    <item android:id="@+id/menu_delete"
          android:title="删除"/>
</menu>
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
    // 准备选项菜单,加载自定义菜单资源
    getMenuInflater().inflate(R.menu.context_menu, menu);
    return super.onPrepareOptionsMenu(menu);
}

@Override
public boolean onOptionsItemSelected(MenuItem item) {
    // 处理点击事件
    switch (item.getItemId()) {
        case R.id.menu_edit:
            // 处理编辑操作
            return true;
        case R.id.menu_delete:
            // 处理删除操作
            return true;
        default:
            return super.onOptionsItemSelected(item);
    }
}

上下文操作栏是另一种提供交互的方式,可以在其中放置操作按钮,用户可以点击它们执行相关操作。

5.2 Android版本兼容性考虑

不同的Android版本拥有不同的API支持,因此在开发过程中,需要考虑兼容性问题,确保应用在所有版本上都能正常运行。

5.2.1 兼容不同版本的API差异

为了兼容不同版本的API差异,开发者可以采取多种策略。一种常见的方法是使用Android Support Library,它为旧版本的Android提供了新API的功能。

dependencies {
    implementation 'com.android.support:appcompat-v7:28.0.0'
}

然后可以在代码中检查API级别,并相应地使用库中的API。

if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) {
    // 对于API级别低于16的设备,使用Support Library中的功能
    ViewCompat.setElevation(myView, dpToPx(2));
} else {
    // 对于API级别16及以上,使用原生API
    ViewCompat.setElevation(myView, dpToPx(2));
}

5.2.2 使用兼容库和条件编译

使用兼容库(如AndroidX)是另一种保证应用兼容性的方法。AndroidX提供了更广泛的库和组件支持,是Support Library的更新和改进版本。

dependencies {
    implementation 'androidx.appcompat:appcompat:1.1.0'
}

另外,条件编译是一种在编译时根据不同的条件选择性地编译代码的手段。在Java中,可以使用预处理指令来实现。

#ifdef ALEXA
    // 仅在使用ALEXA预处理器定义时编译
    // 具体代码
#endif

在Android Studio中,可以通过在build.gradle文件中设置不同的build types或flavors来使用条件编译。

android {
    ...
    buildTypes {
        release {
            // 定义发布版本
        }
        debug {
            // 定义调试版本
        }
    }
    ...
}

通过以上方法,开发者可以确保通讯录应用不仅在现代设备上运行良好,也能兼容早期版本的Android设备。这样既能保证用户基数,也能提升应用的可维护性和长期的市场竞争力。

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:在Android开发中,实现通讯录功能对于管理用户个人信息及通信至关重要。本教程通过一个基础的Demo项目,详细指导初学者如何在Android平台上从获取数据到界面展示完整实现通讯录功能。涵盖内容包括使用Content Provider和ContactsContract类操作联系人数据,获取必要的权限,查询联系人信息,以及展示数据。同时,讲解了如何处理用户交互和在不同Android版本上适配联系人API。项目文件"AndroidContact_beta9"提供了一个完整的项目源码参考,帮助开发者深入理解通讯录功能的实现原理,并能在实际开发中应用。

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

基于Vue 3实现的Cesium大屏可视化项目源代码,展示了Cesium的一些基础示例,该项目是个人毕设项目,答辩评审分达到98分,代码都经过调试测试,确保可以运行!欢迎下载使用,可用于小白学习、进阶。该资源主要针对计算机、通信、人工智能、自动化等相关专业的学生、老师或从业者下载使用,亦可作为期末课程设计、课程大作业、毕业设计等。项目整体具有较高的学习借鉴价值!基础能力强的可以在此基础上修改调整,以实现不同的功能。 基于Vue 3实现的Cesium大屏可视化项目源代码,展示了Cesium的一些基础示例基于Vue 3实现的Cesium大屏可视化项目源代码,展示了Cesium的一些基础示例基于Vue 3实现的Cesium大屏可视化项目源代码,展示了Cesium的一些基础示例基于Vue 3实现的Cesium大屏可视化项目源代码,展示了Cesium的一些基础示例基于Vue 3实现的Cesium大屏可视化项目源代码,展示了Cesium的一些基础示例基于Vue 3实现的Cesium大屏可视化项目源代码,展示了Cesium的一些基础示例基于Vue 3实现的Cesium大屏可视化项目源代码,展示了Cesium的一些基础示例基于Vue 3实现的Cesium大屏可视化项目源代码,展示了Cesium的一些基础示例基于Vue 3实现的Cesium大屏可视化项目源代码,展示了Cesium的一些基础示例基于Vue 3实现的Cesium大屏可视化项目源代码,展示了Cesium的一些基础示例基于Vue 3实现的Cesium大屏可视化项目源代码,展示了Cesium的一些基础示例基于Vue 3实现的Cesium大屏可视化项目源代码,展示了Cesium的一些基础示例基于Vue 3实现的Cesium大屏可视化项目源代码,展示了Cesium的一些基础示例基于Vue 3实现的Cesium大屏可视化项目源代码,展示了
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值