Skip to content

Android 学习

第一行代码——Android(第2版)读书笔记

项目文件结构

上级目录 名称      说明      类型
.gitignore 版本控制文件 文件
local.properties 配置本机 android sdk 目录 文件
build.gradle 项目全局的构建脚本 文件
settings.gradle 项目引入文件 文件
Source.iml IDE 项目文件 文件
gradlew 执行 gradle linux 下使用 文件
gradlew.bat 执行 gradle windows 下使用 文件
.gradle 程序自动管理目录 目录
.idea 程序自动管理目录 目录
gradle gradle wrapper 配置文件 目录
app 代码、资源目录 目录
app app.iml IDE 项目文件 目录
app build.gradle 项目构建脚本 文件
app proguard-rules.pro 项目混淆规则 文件
app build 编译时自动创建文件 目录
app libs 需要引用的第三方 jar 包 目录
app src 项目代码 目录
app\src androidTest android 测试用例 目录
app\src test 自动化测试代码 目录
app\src main 主代码目录 目录
app\src\main AndroidManifest.xml android 项目配置文件 文件
app\src\main java 代码目录 目录
app\src\main res 代码引用的资源 目录

AndroidManifest.xml

所有活动都要添加到 AndroidManifest.xml 中

<!-- .HelloWorl 表示相应的 java 类,package 引用包名,这里可以简写用.开头, MAIN 表示是主活动,LAUNCHER 表示启动项 -->
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.x.demo.xapplication">
<activity android:name=".HelloWorld">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
</activity>
</manifest>

视图类

public class HelloWorld extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // 加载布局资源文件 Res\layout\activity_hello_world.xml
        setContentView(R.layout.activity_hello_world); 
    }
}

资源文件夹

文件夹名格式 类型_分辨率, 类型有如下几种 drawable: 图片 , mipmap: 图标, values:字符串、样式、颜色 等配置

资源使用

res/values/strings.xml 文件中定义
<resources>
    <string name="app_name">HelloWorld</string>
</resources>

// 代码中引用 R.string.app_name
// xml中引用 @string/app_name

// xml 增加一个 id
android:id="@+id/button_1" # 表示添加一个类型为 id 名称为 button_1 的资源
代码中使用  findViewById(R.id.button_1) 

// 再比如 AndroidManifest.xml 文件
<application
        android:allowBackup="true" 
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme" />

build.gradle

编译配置文件

全局 build.gradle

buildscript {    
    repositories { ## 指定仓库
        google()
        jcenter() 
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:3.2.1' ## 指定编译器,这是给 android 用的
    }
}

allprojects {
    repositories { ## 指定仓库 
        google()
        jcenter()
    }
}

app build.gradle

apply plugin: 'com.android.application' # 表明这是一个 app
# apply plugin: 'com.android.library' # 表明这是一个为模块

android {
    compileSdkVersion 28 # 编译版本
    defaultConfig {
        applicationId "com.x.demo.xapplication"  # 项目的包名
        minSdkVersion 15 # 最低支持的系统版本
        targetSdkVersion 28 # 充分测试的系统版本。系统根据这个版本决定运行时支持什么特性
        versionCode 1 # 项目版本号
        versionName "1.0" # 项目版本名
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false # 要不要对项目进行混淆
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' # 混淆规则,proguard-android.txt:系统提供的规则 proguard-rules.pro:当前项目中可自定义的规则 
        }
    }
}

dependencies { # 定义当前项目的所有依赖包, 本地依赖,库依赖,远程依赖
    implementation fileTree(dir: 'libs', include: ['*.jar']) # 本地依赖,将 libs 下所有 *.jar 的文件都编译进去
    implementation 'com.android.support:appcompat-v7:28.0.0' # 远程依赖 com.android.support:公司名, appcompat-v7:组件名, 28.0.0: 版本号, 如果本地没有缓存,将从远程下载(应该就是全局 build.gradle 中定义的远程库了)
    implementation 'com.android.support.constraint:constraint-layout:1.1.3'
    testImplementation 'junit:junit:4.12' # 测试使用
    androidTestImplementation 'com.android.support.test:runner:1.0.2' # android 测试使用
    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
              # 还有一个库依赖,这里没有例子
}

日志

Log.v() # verbose
Log.d() # debug
Log.i() # info
Log.w() # warn
Log.e() # errro

Log.v(this.getLocalClassName(), "启动");

活动视图

添加按钮

// 如果多次绑定单击监听事件,则最后一次生效    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        // 1. 获得按钮 
        View button = this.findViewById(R.id.button_1);
        // 2. 1 将 this 绑定到 click 事件上
        button.setOnClickListener(this);
        // 2. 2 new 一个 OnClickListener 对象绑定到 click 事件上
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                // 3.1 处理单击事件 
                HelloWorld.this.showToast(String.format("you clocked button 1:%s", this.getClass().getSimpleName()));
            }
        });
    }

    @Override
    public void onClick(View v) {
        // 3.2 处理单击事件 
        this.showToast(String.format("you clocked button 1:%s", this.getLocalClassName()));
    }

添加菜单

    // 初化菜单 
    @Override
    public boolean onCreateOptionsMenu(Menu menu) {     
        getMenuInflater().inflate(R.menu.main, menu);
        return true;
    }

    // 处理菜单消息 
    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()){
            case R.id.add_item:    break;
            case R.id.remove_item: break;
        }
        return super.onOptionsItemSelected(item);
    }

运行一个 Intent

显式运行

Intent intent = new Intent(this, XmessageBox.class);
this.startActivity(intent);

隐式运行

定义一个活动

<activity android:name=".XmessageBox">
    <intent-filter>
        <action android:name="com.x.demo.xapplication.START"/>
        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="com.x.demo.xapplication.XSEARCH" />
    </intent-filter>
</activity>

调用

Intent intent = new Intent("com.x.demo.xapplication.START"); # 定义 Intent 的 action 及 category , DEFAUTL 可以不用写
intent.addCategory("com.x.demo.xapplication.XSEARCH"); # 如果有多个 category 可以继续添加 
this.startActivity(intent); # 启动

显示网页

Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setData(Uri.parse("https://www.baidu.com"));
this.startActivity(intent);

// 默认为浏览器响应,如果AndroidManifest加入如下配置, 则系统会提示使用者选择浏览器还是内置 activity 响应
<action android:name="android.intent.action.VIEW" />
<data android:scheme="http" />

// 打电话 
Intent intent = new Intent(Intent.ACTION_DIAL);
intent.setData(Uri.parse("tel:10086"));

传输数据

// 发送方
Intent intent = new Intent(this, XmessageBox.class);
intent.putExtra("xData", "hallowed.");
this.startActivity(intent);

// 接收方
Intent intent = this.getIntent();
String xdata = intent.getStringExtra("xData"); // 或 intent.getExtra("xData");
Log.v(this.getLocalClassName(),  String.format("找到数据:%s", xdata ));

// 接收方可以实现, 创建一个启动辅助函数
public static Intent actionStart(Activity activity, String title, String message, int requestCode){

    Intent intent = new Intent(activity, XmessageBox.class);
    intent.putExtra("xTitle", title);
    intent.putExtra("xMessage", message);
    activity.startActivityForResult(intent, requestCode);

    return intent;
}

返回数据

// 第一页
Intent intent = new Intent(this, XmessageBox.class); // 创建需要显示的活动
intent.putExtra("xData", "hallowed."); // 添加传输值
this.startActivityForResult(intent, 10012); // 要求完成后回调

/*** 活动关闭回调函数*/
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
    switch (requestCode){
        case 10012:
            int xReturn = data.getIntExtra("xReturn", -1);
            Log.d(this.getLocalClassName(), String.format("获取状态:%s 返回值:%s: ", resultCode, xReturn));
            break;
        }
        super.onActivityResult(requestCode, resultCode, data);
    }
// 第二页

Intent intent = this.getIntent();
intent.putExtra("xReturn", 12345); // 设置一个返回的内容
this.setResult(RESULT_OK, intent); // 设置返回值,只能是 int, 相当于 showdialog 的返回值
this.finish(); // 关闭窗口

活动周期

活动周期图

    /** 创建时, 进行一些初始化 */
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);     }

    /** 不可见至可见的时候调用 */
    @Override
    protected void onStart() {
        super.onStart();     }

    /** 活动位于 task 栈顶,准备与用户交互时 */
    @Override
    protected void onResume() {
        super.onResume();     }

    /** 准备启动另外一个活动时,可以释放一些资源或保存一些状态 */
    @Override
    protected void onPause() {
        super.onPause();     }

    /** 活动不可见时调用。比如准备启动另外一个活动时。如果启动对话框,则不会被调用 */
    @Override
    protected void onStop() {
        super.onStop();     }

    /** 活动被销毁时调用  */
    @Override
    protected void onDestroy() {
        super.onDestroy();     }

    /** 活动被重新启用时 */
    @Override
    protected void onRestart() {
        super.onRestart();     }

活动被回收

系统资源不足时,活动会被回收。活动在回收前会调用 onSaveInstanceState, 将相关值写入到参数中 outState.putXXX 之类的。然后在活动 onCreate 中读回相关值

启动模式

<!-- AndroidManifest.xml 中设置 -->
<activity android:name=".HelloWorld" android:launchMode="singleInstance">

getTaskId() 返回当前栈 id

standard: 默认模式。使用时创建,不活动时销毁

singleTop: 栈项的活动任务,再启动时使用自身。不活动时销毁

singleTask: 活动时如果栈中已经存在,则使用该实例,并把这个活动之上的活动全部出栈。

singleInstance: 会保存在单独的一个栈中。比如 A栈 中 活动1 调用, 在 B栈中创建 singleInstance 的活动2 ,再调用 A栈中的 活动3. 活动3 back 时是会抬到 活动1 的,再 back A栈中无活动,于是就到 B 栈。 再 back B 栈无活动,程序退出。

关闭程序

  • 实现一个基类,维护一个列表,在 onCreate 时将自动加入到列表,onDestory 时将自身移出列表。在关闭时,调用列表中所有元素的 finish()
  • 关闭自身进程 android.os.Process.killProcess(android.os.Process.myPid());

界面

常用属性

layout_width\layout_height: 定义宽高, 
    match_parent(fill_parent):占满整个父窗口
    wrap_content: 刚好包住内容
android:layout_weight='1':定义控件在水平方向上的权重,定义后 layout_width 失效
android:hint="这是提示内容"
android:gravity: 文字排列方式 
android:visibility="invisible":控制可见性 gone:不可见,不占空间; invisible:不可见,占空间; visible:可见

显示一个对话框

AlertDialog.Builder dialog = new AlertDialog.Builder(this);
dialog.setTitle("Title");
dialog.setMessage("message");
dialog.setCancelable(false);
dialog.setPositiveButton("ok", listener);
dialog.setNegativeButton("cancel", listener);

dialog.show();

布局

  • 可以使用 LinearLayout 、FrameLayout 等布局控件

  • 可以自定义控件,然后使用 <include layout="@layout/title"> 引入自定义控件

  • 继承 LinearLayout(或别的 layout), 初始化时调用 LayoutInflater.form(context).inflate(R.layout.title, this) 将 this 与 布局进行捆绑

将自定义布局加入到活动页面中 , 然后可以在自定义类中实现一个函数,作为通用函数

listview

布局

```java // 为 listview 准备数据,及数据对应的项的视图, android.R.layout.simple_list_item_1 为系统内置 ArrayAdapter arrayAdapter = new ArrayAdapter(this, android.R.layout.simple_list_item_1); arrayAdapter.add("x123456");

ListView listView = this.findViewById(R.id.xListView); listView.setAdapter(arrayAdapter); // 将数据绑定视图

// 如果要自定义 listview 中的项,则自定义一个 ArrayAdapter, 并重载 getView, 在函数中生成相应的 view (内部调用 LayoutInflater.form(context).inflate) ,填充数据并返回
```

单击
listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
    @Override
    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
        var item = arrayAdapter.getItem(position); // 返回指定位置对应的项目, 并使用                 
    }
});

RecyclerView

布局
  1. 界面中加入 RecyclerView,
  2. 在 onCreate 中将 RecyclerView 与 数据源进行绑定
  3. 重载数据源处理的相关函数,返回每一项的视图及监听相应的事件
var recyclerView = (RecyclerView) findViewById(R.id.recycler_view);

// 设置布局, 可以使用不同的布局,这就是比 listview 方便的地方
var layoutManager = new StaggeredGridLayoutManager(3, StaggeredGridLayoutManager.VERTICAL);
// var layoutManager = new LinearLayoutManager(this); // 比如换个布局管理器
recyclerView.setLayoutManager(layoutManager);

FruitAdapter adapter = new FruitAdapter(fruitList);
recyclerView.setAdapter(adapter); // 设置数据源

数据源管理

public class FruitAdapter extends RecyclerView.Adapter<FruitAdapter.ViewHolder>{

    private List<Fruit> mFruitList;

    /**  为每一项设置一个对象,管理对应项的视图内容 */
    static class ViewHolder extends RecyclerView.ViewHolder {
        View fruitView; ImageView fruitImage; TextView fruitName;

        public ViewHolder(View view) {
            super(view);
            fruitView = view;
            fruitImage = (ImageView) view.findViewById(R.id.fruit_image);
            fruitName = (TextView) view.findViewById(R.id.fruit_name);
        }
    }

    /** 为每一项创建一个相应的视图 */
    @Override
    public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        // 通过资源布局创建一个视图对象
        View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.fruit_item, parent, false);
        final ViewHolder holder = new ViewHolder(view);

        // 创建单击事件, 可以直接绑定至子项中的某一个元素
        holder.fruitView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                int position = holder.getAdapterPosition();
                Fruit fruit = mFruitList.get(position);
                Toast.makeText(v.getContext(), "you clicked view " + fruit.getName(), Toast.LENGTH_SHORT).show();
            }
        });

        return holder;
    }

    /** 将视图与某个位置的内容进行绑定 */
    @Override
    public void onBindViewHolder(ViewHolder holder, int position) {
        Fruit fruit = mFruitList.get(position);
        holder.fruitImage.setImageResource(fruit.getImageId());
        holder.fruitName.setText(fruit.getName());
    }
}

一个聊天消息窗口

设置一个聊天内容布局,里面可以有接收消息与发送消息的样式。根据消息的类型自动设置

初始化

var msgRecyclerView = (RecyclerView) findViewById(R.id.msg_recycler_view);
LinearLayoutManager layoutManager = new LinearLayoutManager(this); // 设置对话窗口的布局
msgRecyclerView.setLayoutManager(layoutManager);
var adapter = new MsgAdapter(msgList); // 初始化并设置数据源
msgRecyclerView.setAdapter(adapter);

添加消息内容

// 在按钮事件中
Msg msg = new Msg(content, Msg.TYPE_SENT); // 创建一个聊天消息项
msgList.add(msg); // 将消息添加到列表中
adapter.notifyItemInserted(msgList.size() - 1); // 当有新消息时,刷新ListView中的显示, 其它还有更新、删除函数
msgRecyclerView.scrollToPosition(msgList.size() - 1); // 将ListView定位到最后一行
inputText.setText(""); // 清空输入框中的内容

数据源管理器

// MsgAdapter

/** 初始化聊天中某一项的视图对象 */
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {    
    View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.msg_item, parent, false);
    return new ViewHolder(view);
}

/** 设置聊天中某一项的视图对象 */
public void onBindViewHolder(ViewHolder holder, int position) {
    Msg msg = mMsgList.get(position);
    if (msg.getType() == Msg.TYPE_RECEIVED) {
        // 如果是收到的消息,则显示左边的消息布局,将右边的消息布局隐藏
        holder.leftLayout.setVisibility(View.VISIBLE);
        holder.rightLayout.setVisibility(View.GONE);
        holder.leftMsg.setText(msg.getContent());
    } else if(msg.getType() == Msg.TYPE_SENT) {
        // 如果是发出的消息,则显示右边的消息布局,将左边的消息布局隐藏
        holder.rightLayout.setVisibility(View.VISIBLE);
        holder.leftLayout.setVisibility(View.GONE);
        holder.rightMsg.setText(msg.getContent());
    }
}

碎片

碎片是可以嵌入到活动中的 UI 片段,可以充分利用屏幕的空间

创建

  1. 与普通活动一样创建布局

  2. 为布局创建对应的从 Fragment 继承的类, 并重载 onCreateView ,将 类与布局进行绑定

java public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.left_fragment, container, false); // 将布局与碎片进行绑定 return view; }

  1. 添加碎片

    1. 在活动视图中加入碎片

      ```java
      <!-- 将 name 设置为类名, 其它与普通活动布局一致 -->
      <fragment
      android:id="@+id/left_fragment"
      android:name="com.example.fragmenttest.LeftFragment"
      android:layout_width="match_parent"
      android:layout_height="match_parent"/>
      ```
      
    2. 动态添加

      1. 在活动视图中添加一个 layout作为碎片的容器,如下 
            ```xml
            <FrameLayout
              android:id="@+id/right_layout" 
              android:layout_height="match_parent" android:layout_weight="3" />
            ```
      2. 在活动类中加入如下代码
            ```java
            FragmentManager fragmentManager = getSupportFragmentManager();
            FragmentTransaction transaction = fragmentManager.beginTransaction();
            // 注意是替换掉了。需要的话使用 fragmentManager.findFragmentById(R.id.right_layout) 找回
            transaction.replace(R.id.right_layout, fragment); 
            transaction.addToBackStack(null); // 将操作入栈,这样按 back 时可以返回未替换前的状态 
            transaction.commit();
            ```
      

碎片相互通信

// 活动中找碎片 
FragmentManager fragmentManager = getSupportFragmentManager();
fragmentManager.findFragmentById(R.id.right_layout)

// 碎片中找活动
FragmentActivity activity = this.getActivity(); // 返回当前碎片所在的活动

// 碎片找碎片, 1. 找到所在活动 2. 调用活动方法找到碎片 
FragmentActivity activity = this.getActivity();
FragmentManager manager = activity.getSupportFragmentManager();
fragmentManager.findFragmentById(R.id.right_layout)

碎片生命周期

img

根据设备限定符加载不同布局

指定不同屏幕大小 small\normal\large\xlarge 指定一个范围 ldpi/mdpi/hdpi/xhdpi/xxhdpi 0-120/160/240/320/480dpi 指定不同方向 land/port 横屏/竖屏 指定最小值 swxxxxdp sw600dp

自适应程序

  1. 为不同设备设计主活动,根据不同的屏幕大小加载不同的碎片

  2. 在碎片的 onActivityCreated 中判断一当前活动中其它碎片的情况

java // 在碎片中获取当前所在的活动,然后查找其它碎片是否存在 if (getActivity().findViewById(R.id.news_content_layout) != null) { isTwoPane = true; // 可以找到news_content_layout布局时,为双页模式 } else { isTwoPane = false; // 找不到news_content_layout布局时,为单页模式 } 3. 在处理程序时,根据没同的情况进行不同的处理

java News news = mNewsList.get(holder.getAdapterPosition()); if (isTwoPane) { // 如果当前在其它碎片的存在,则直接将内容加载到此碎片中 NewsContentFragment newsContentFragment = (NewsContentFragment) getFragmentManager().findFragmentById(R.id.news_content_fragment); newsContentFragment.refresh(news.getTitle(), news.getContent()); } else { // 如果没有其它碎片的存在,直接加载一个活动 NewsContentActivity.actionStart(getActivity(), news.getTitle(), news.getContent()); }

广播机制

标准广播: 异步执行广播,所有客户端几乎同一时间接收。 有序广播: 同一时刻只有一个接收器接收。优先级高的接收器先接收,接收完成后可以向后传播或停止传播。

接收系统消息

动态注册

需要程序运行,在页面中调用,传入页面对象

  1. 继承 BroadcastReceiver, 重写 onReceive

  2. new IntentFilter , 添加需要接收的广播类型

  3. 调用 registerReceiver 进行监听

  4. 调用 unregisterReceiver 释放资源

  5. 例子, 在活动中调用

```java // 接收器 class NetworkChangeReceiver extends BroadcastReceiver { public void onReceive(Context context, Intent intent) {} }

// 监听 intentFilter = new IntentFilter(); intentFilter.addAction("android.net.conn.CONNECTIVITY_CHANGE"); networkChangeReceiver = new NetworkChangeReceiver(); this.registerReceiver(networkChangeReceiver, intentFilter);

// 释放 this.unregisterReceiver(networkChangeReceiver); ```

静态注册

可以使用向导创建,右键,新建,广播接收器, 然后它会创建接收器类及在 AndroidManifest.xml 中添加相关的节点。 在节点中添加需要注册消息类型(intent-filter)即可, 注意有可能还有请求相应的权限。

<receiver
            android:name=".MyBroadcastReceiver" <!-- 自动生成对应的类 -->
            android:enabled="true" <!-- 是否启用 -->
            android:exported="true"> <!-- 是否接受全局消息 -->
            <intent-filter android:priority="100">
                <action android:name="com.example.broadcasttest.MY_BROADCAST"/> <!-- 需要接收的消息的类型 -->
            </intent-filter>
        </receiver>

发送自定义广播

// 1. 静态或是动态注册接收器

// 2. 发送消息 
Intent intent = new Intent("com.example.broadcasttest.LOCAL_BROADCAST");
this.sendBroadcast(intent); 

发送有序广播

// 1. 与发送标准广播类型, 调用函数不一样
Intent intent = new Intent("com.example.broadcasttest.LOCAL_BROADCAST");
this.sendOrderedBroadcast(intent, null); // 第二个为权限值,先传入 null
// 2. AndroidManifest.xml 中定义一下接收优先级
<intent-filter android:priority="100" />
// 3. 在 onReceive 中可以调用 abortBroadcast(); //中断消息传播     

本地广播

// 与系统广播一致, 使用 LocalBroadcastManager

// 1. 发送广播
localBroadcastManager = LocalBroadcastManager.getInstance(this); // 获取实例
Intent intent = new Intent("com.example.broadcasttest.LOCAL_BROADCAST");
localBroadcastManager.sendBroadcast(intent); // 发送本地广播

// 2. 接收广播
intentFilter = new IntentFilter();
intentFilter.addAction("com.example.broadcasttest.LOCAL_BROADCAST");
localReceiver = new LocalReceiver();
localBroadcastManager.registerReceiver(localReceiver, intentFilter); // 注册本地广播监听器

// 3. 注销广播接收器
localBroadcastManager.unregisterReceiver(localReceiver);

接收下线消息

  1. 订制一个活动基类
  2. 基类在 onCreate 时将自身加入到一个静态列表中,在 onDestroy 中将自身从静态列表中移除
  3. 基类在 onResume 中监听广播,onPause 中停止监听广播。 onResume / onPause 是活动在栈顶与非栈顶时的消息 。所以广播接收者必定会在顶层时才能接收到消息

  4. 接收到消息时,调用静态列表中所有活动的 finish 函数,然后在最后调用登录活动

java ActivityCollector.finishAll(); // 销毁所有活动 Intent intent = new Intent(context, LoginActivity.class); context.startActivity(intent); // 重新启动 LoginActivity

请求权限

接收事件时可能需要进行一些系统调用,需要在 AndroidManifest.xml 中添加权限要求,比如下面要求查询网络的权限

<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

持久化数据

文件形式存储

标准文件流

/data/data//files

保存

public void save(String inputText) {
    FileOutputStream out = null;
    BufferedWriter writer = null;
    try {
        // Context.MODE_APPEND: 追加 Context.MODE_PRIVATE: 覆盖
        out = openFileOutput("data", Context.MODE_PRIVATE); 
        writer = new BufferedWriter(new OutputStreamWriter(out));
        writer.write(inputText);
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        try {
            if (writer != null) {
                writer.close();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

读取

    public String load() {
        FileInputStream in = null;
        BufferedReader reader = null;
        StringBuilder content = new StringBuilder();
        try {
            in = openFileInput("data");
            reader = new BufferedReader(new InputStreamReader(in));
            String line = "";
            while ((line = reader.readLine()) != null) {
                content.append(line);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (reader != null) {
                try {
                    reader.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return content.toString();
    }

SharedPreference 存储

相当于字典

// context 中实现
SharedPreferences preferences = this.getSharedPreferences("fileName", this.MODE_PRIVATE);
// activity 中实现, 以当前活动名作为文件名
SharedPreferences preferences = this.getPreferences(this.MODE_PRIVATE);
// 静态函数,传入 context , 以当前包名作为文件名
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);

// 设置值
SharedPreferences.Editor edit = preferences.edit();
edit.putInt("key", 123);
edit.putLong("key", 123);
edit.clear(); // 清空所有数据 
editor.apply(); // 提交保存

// 返回值
preferences.getInt("key", -1);
preferences.getLong("key", -1);

SQLITE

  1. 继承 SQLiteOpenHelper

```java // 重载创建数据表函数 @Override public void onCreate(SQLiteDatabase db) { db.execSQL(CREATE_BOOK); db.execSQL(CREATE_CATEGORY);
}

// 重载更新数据表函数, 需要 new MyDatabaseHelper 时, 更新版本号才会被调用 @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { db.execSQL("drop table if exists Book"); db.execSQL("drop table if exists Category"); onCreate(db); } ```

  1. 打开数据库

java // 最后一个是版本号, 版本号更新 onUpgrade 会被调用 dbHelper = new MyDatabaseHelper(this, "BookStore.db", null, 2); // 打开只读数据库 dbHelper.getReadableDatabase(); // 打开读写数据库 dbHelper.getWritableDatabase();

  1. 插入数据

java SQLiteDatabase db = dbHelper.getWritableDatabase(); ContentValues values = new ContentValues(); values.put("name", "The Da Vinci Code"); values.put("pages", 454); values.put("price", 16.96); db.insert("Book", null, values); // 插入第一条数据, 第二个参数先默认填入 null, 返回插入的数据的 rowid values.clear();

  1. 更新数据

java SQLiteDatabase db = dbHelper.getWritableDatabase(); ContentValues values = new ContentValues(); values.put("price", 10.99); // 使用 where 找到数据进行更新 db.update("Book", values, "name = ?", new String[] { "The Da Vinci Code" });

  1. 删除数据

java SQLiteDatabase db = dbHelper.getWritableDatabase(); db.delete("Book", "pages > ?", new String[] { "500" });

  1. 查询

```java SQLiteDatabase db = dbHelper.getWritableDatabase(); // 查询Book表中所有的数据 // Cursor query(String table, String[] columns, String selection, String[] selectionArgs, String groupBy, String having, String orderBy) // Cursor cursor = db.query("Book", new String[] {"rowid as rowid", "*"}, null, null, null, null, null); Cursor cursor = db.query("Book", null, null, null, null, null, null); if (cursor.moveToFirst()) { do {
long rowid = cursor.getLong(cursor.getColumnIndex("rowid")); String name = cursor.getString(cursor.getColumnIndex("name")); String author = cursor.getString(cursor.getColumnIndex("author")); int pages = cursor.getInt(cursor.getColumnIndex("pages")); double price = cursor.getDouble(cursor.getColumnIndex("price"));

} while (cursor.moveToNext());

} cursor.close(); ```

  1. 直接操作

java db.execSQL(); db.rawQuery();

LitePal

准备工作

  1. 修改 build.gradle

java # 在 app 的 build.gradle 中添加引用 dependencies { compile 'org.litepal.android:core:1.3.2' } ```

  1. 创建 litepal.xml

xml <!-- main/assets/litepal.xml --> <?xml version="1.0" encoding="utf-8"?> <litepal> <!-- 数据库名 --> <dbname value="BookStore" ></dbname> <!-- 数据库版本, 只要版本增加,数据库将自动升级 --> <version value="2" ></version> <list> <mapping class="com.example.litepaltest.Book"></mapping> <mapping class="com.example.litepaltest.Category"></mapping> </list> </litepal>

  1. 修改 AndroidManifest.xml

```xml

```

  1. 新建 ORM 类

```java // 类需要继承自 DataSupport package com.example.litepaltest; import org.litepal.crud.DataSupport; public class Book extends DataSupport {

   private int id;
   private String author;
   private double price;
   public int getId() {
       return id;    }
   public void setId(int id) {
       this.id = id;    }
   public String getAuthor() {
       return author;    }
   public void setAuthor(String author) {
       this.author = author;    }
   public double getPrice() {
       return price;    }
   public void setPrice(double price) {
       this.price = price;    }

} ```

使用

```java // 将会初始化数据库 LitePal.getDatabase(); Connector.getDatabase();

// 添加数据 Book book = new Book(); book.setAuthor("Dan Brown"); book.setPrice(16.96); book.save();

// 更新数据, 注意,因为 new Book 时,所有值为默认值,所以如果某个值被更新为默认值时,是不会写入到数据库中的 Book book = new Book(); book.setPrice(14.95); book.updateAll("name = ? and author = ?", "The Lost Symbol", "Dan Brown"); // 将某个值更新为默认值 Book book = new Book(); book.setToDefault("price"); book.updateAll();

// 删除1, 我未测试 Book book = new Book(); book.setId(1); book.delete();

// 删除2 DataSupport.deleteAll(Book.class, "price < ?", "15"); // 不指定条件,删除所有的

// 删除3 // 调用原生 sql

// 查询 List books = DataSupport.findAll(Book.class); // 列出所有数据 Book book = DataSupport.findFirst(Book.class); // 找到第一本 Book book = DataSupport.findLast(Book.class); // 找到最后一本 List books = DataSupport.select("author", "price").find(Book.class); // 只找指定的属性 // 加入更多查找条件 List books1 = DataSupport .select("author", "price") .where("pages > ? ", "400") .order("price desc") .limit(10) .offset(5) .find(Book.class);

// 原生 sql Cursor cursor = DataSupport.findBySQL("select * from Bok where price > ?; ", "300"); ```

序列化

android 中提供了序列化对象接口( Serializable),但是书中没有提到,等可能用到时再说

内容提供器

为了在不同应用之间共享数据,可以使用内容提供器进行数据共享,

运行时权限

<!-- AndroidManifest.xml 中指定需要的权限 -->
<uses-permission android:name="android.permission.CALL_PHONE" />

在低于 6.0 系统上会在安装时提示权限要求,6.0 或以上版本应用在使用到相关权限时再请求

andorid 中权限分为正常权限与危险权限,正常权限会自动授权,但是危险权限会提示用户授权。相关见 [权限说明]: https://developer.android.com/guide/topics/security/permissions?hl=zh-cn

请求权限流程

在按钮函数中判断是否已经存在权限了, 如果有权限直接外呼,如果没有权限,请求权限并返回 。用户给予权限后通过回调函数得到授权情况,并继续调用外呼函数。 例子如下:以后可以将整个权限处理流程都包到一个函数中,函数参数为调用函数及要求的权限。


    /* 回调接口,如果没有权限,将函数包起来等待回调 */
    interface IDoable {
        void doIt();
    }

    /** 生成一个不重复序号, 供回调使用 */
    static AtomicInteger atomicInteger = new AtomicInteger();

    /** 缓存请求权限处理后的处理函数 */
    Map<Integer, IDoable> doItMap = new HashMap<>();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Button makeCall = (Button) findViewById(R.id.make_call);
        makeCall.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {

                // 取得 activity 的引用
                final MainActivity mainActivity = MainActivity.this;
                // 需要的外呼权限
                String callPhonePermission = Manifest.permission.CALL_PHONE;
                // 判断权限,如果有,直接调用 
                int result = ContextCompat.checkSelfPermission(mainActivity, callPhonePermission);
                if (result == PackageManager.PERMISSION_GRANTED) {
                    Log.d(mainActivity.getLocalClassName(), "有拨号权限");
                    mainActivity.call();
                    return;
                }

                // 如果没有权限,包装一个IDoable 对象,请求权限,并等待 onRequestPermissionsResult 的回调
                Log.d(mainActivity.getLocalClassName(), "没有拨号权限");
                int requestCode = atomicInteger.getAndIncrement();
                Log.d(mainActivity.getLocalClassName(), String.format("请求拨号权限: %s", requestCode));
                doItMap.put(requestCode, new IDoable() {
                   // 包装函数
                    @Override
                    public void doIt() { mainActivity.call(); }
                });
                // 请求权限 
                ActivityCompat.requestPermissions(mainActivity, new String[]{callPhonePermission}, requestCode);
            }
        });
    }

    /**
     * 权限请求处理后的回调函数
     * @param requestCode
     * @param permissions  要求的请求
     * @param grantResults 对应 permissions 的请求是否通过 (==PackageManager.PERMISSION_GRANTED)
     */
    @Override
    public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
        // 查找有没有对应的处理函数,如果就直接调用
        IDoable iDoable = doItMap.get(requestCode);
        if (iDoable == null) {
            Log.d(this.getLocalClassName(), String.format("权限请求 %s 没有处理函数", requestCode));
            return;
        }

        Log.d(this.getLocalClassName(), String.format("开始处理请求 %s", requestCode));
        // 简单的认为第一项就是需要的权限, todo: doIt 调用
        if (grantResults != null && grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
            iDoable.doIt();
        }
    }

    /** 呼叫电话 */
    private void call() {
        try {
            Intent intent = new Intent(Intent.ACTION_CALL);
            intent.setData(Uri.parse("tel:10086"));
            startActivity(intent);
        } catch (SecurityException e) {
            // 如果没有权限就进行记录
            e.printStackTrace();
            Log.e(this.getLocalClassName(), "外呼电话发生错误", e);
            Toast.makeText(this, "You denied the permission", Toast.LENGTH_SHORT).show();
        }
    }

使用内容提供器

以读取电话号码 本为例子

// uri 为读取器提供的唯一定义符,一般使用包名来命名, 比如 Uri.parse("content://com.example.app.provider/table1") 之类的形式,前面定位应用,后面定位应用中的项目
Uri contentUri = ContactsContract.CommonDataKinds.Phone.CONTENT_URI;
// 打开读取器
ContentResolver contentResolver = this.getContentResolver();

// 读取联系人, 记得关闭 cursor
// query(Uri 定位符, String[] 查询的列名, String where 限制, String[] where 参数, String 排序 参数)
cursor = contentResolver.query(contentUri, null, null, null, null);
while (cursor != null && cursor.moveToNext()) {
    int nameIdx = cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME);
    int numberIdx = cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER);
    String displayName = cursor.getString(nameIdx);
    String number = cursor.getString(numberIdx);
}

// 添加联系人
ContentValues values = new ContentValues();
values.put("column1", "x");
contentResolver.insert(contentUri, values);

// 更新联系人
ContentValues values = new ContentValues();
values.put("column1", "x");
contentResolver.update(contentUri, values, "column2 = ?", new String[]{"123"});

// 删除联系人
contentResolver.delete(contentUri, "column2 = ?", new String[]{"123"});

创建内容提供器

可以使用向导创建

  • AndroidManifest.xml 中添加
<!-- name:类名; authorities:唯一定位符 -->
<provider   
    android:name=".DatabaseProvider" 
    android:authorities="com.example.databasetest.provider" 
    android:enabled="true" android:exported="true" />
  • 继承一个 ContentProvider 类

可以使用 UriMatcher 辅助解析出 uri

// 继承一个 ContentProvider 类
public class XContentProvider extends ContentProvider {        
    public boolean onCreate() { /** 被查询时调用, 返回 true 表示成功 */ }    
    public Cursor query(Uri uri, String[] strings, String s, String[] strings1, String s1) {    }
    public Uri insert(Uri uri, ContentValues contentValues) {}
    public int delete(Uri uri, String s, String[] strings) {    }
    public int update(Uri uri, ContentValues contentValues, String s, String[] strings) {    }
    public String getType(Uri uri) {
        // 返回 uri 对应的 MIME 类型,
        // vnd.android.cursor.{dir|item}/vnd.{AUTHORITY}.{PATH}
        // 如果 uri 以 / 结尾,则为 dir, 不然为 item
    }
}

多媒体

通知

// 生成通知发生的时间, 为了演示时间如何进行计算,这里将时间修改为一个小时间前
Date date = new Date(System.currentTimeMillis());
Calendar calendar = Calendar.getInstance();
calendar.setTime(date);
calendar.add(Calendar.HOUR, -1);

// 构建一个点击通知后的执行 intent, requestCode 最好每次都不一样
int noteMsgId = 128; // 通知的编号,可以通过这个编号将通知取消
Intent intent = new Intent(this, NotificationActivity.class); // 设置回调使用的 intent
intent.putExtra("noteMsgId", noteMsgId);
intent.putExtra("messageInfoId", 32);
PendingIntent pi = PendingIntent.getActivity(this, 128, intent, 0);

// 1. 构建通知内容, Notification 在不同版本中有不同的区别,所以使用 NotificationCompat 统一构建通知
Notification notification = new NotificationCompat.Builder(this)
.setContentTitle("This is content title") // 通知的标题
.setContentText("This is content text") // 通知的内容
// 标记通知发生的时间, 简单一些可以直接使用 System.currentTimeMillis() ,表示当前时间
.setWhen(calendar.getTimeInMillis()) 
.setSmallIcon(R.mipmap.ic_launcher) // 小图标
.setLargeIcon(BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher)) // 大图标, 需要转为 bitmap
.setContentIntent(pi) // 点击后执行的 intent
.setSound(Uri.fromFile(new File("/system/media/audio/ringtones/Luna.ogg"))) // 播放声音
.setVibrate(new long[]{0, 1000, 1000, 1000}) // 进行振动, 单位数为静止时长,双位数表示振动时长, 需要权限
.setLights(Color.GREEN, 1000, 1000) // 设置消息灯, 灯时长,灭时长
.setDefaults(NotificationCompat.DEFAULT_ALL) // 使用系统默认的声音、振动、灯光设置 (传入不同参数), 就不用自定义这些值了
// setContentText 只能设置短内容,这里可以设置长内容,但是长文件与大图片只能显示一样
.setStyle(new NotificationCompat.BigTextStyle().bigText("这里填写长文本"))
//这里可以设置大图片,但是长文件与大图片只能显示一样
.setStyle(new NotificationCompat.BigPictureStyle().bigPicture(BitmapFactory.decodeResource(getResources(), R.drawable.big_image)))
.setPriority(NotificationCompat.PRIORITY_MAX) // 通知的重要程序
.setAutoCancel(true) // 点击通知后自动消失
.build(); // 构建通知

// 2. 获取通知服务接口
NotificationManager manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
// 3. 发送通知
manager.notify(noteMsgId, notification);

// 4. 主动取消消息
manager.cancel(noteMsgId);

// 显示通知时
Intent intent = this.getIntent();
int noteMsgId = intent.getIntExtra("noteMsgId", -1);
NotificationManager manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
manager.cancel(noteMsgId); // 关闭消息栏上的通知

int messageId = intent.getIntExtra("messageInfoId", -1);
// 使用 messageId 从数据库中读出消息进行显示
Log.d(this.getLocalClassName(), String.format("返回消息编号 %s", messageId));
// 关闭自身
View button = this.findViewById(R.id.close_button);
button.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
                NotificationActivity.this.finish();
}});

摄像头

权限声明

需要设置珍上内容提供器,供摄像机保存照片使用

<!-- AndroidManifest.xml, name 固定,authorities 为对外提供读取的标识符
  因为应用会创建一个文件,让摄像头去保存文件,所以需要一个内容提供器, 这里使用内置类 android.support.v4.content.FileProvider
  需要在 meta-data 中指定共享的路径, 下例在 xml 目录下的 file_paths.xml 中进行配置
-->
<provider
    android:name="android.support.v4.content.FileProvider"
    android:authorities="com.example.cameraalbumtest.fileprovider"
    android:exported="false"
    android:grantUriPermissions="true">
    <meta-data
        android:name="android.support.FILE_PROVIDER_PATHS"
        android:resource="@xml/file_paths" />
</provider>

<!-- xml/file_paths.xml,  external-path 表示共享配置, name 随意, path 为相应的路径, 空为全部 -->
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <external-path name="my_images" path="" />
</paths>

<!-- 早期(android 4.4之前)共享文件访问权限的时候也要申请权限,为了兼容,这里直接请求权限  -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

代码

// 1. 创建File对象,用于存储拍照后的图片
// getExternalCacheDir() 返回应用的缓存目录,其它目录可能要权限,就保存在这里好了,
// sdcard/android/packagename/cache
File outputImage = new File(getExternalCacheDir(), "output_image.jpg");
outputImage.createNewFile(); // 如果 exists() 先 delete()

// 2. 构建图像保存的 uri 供相机保存图像使用
if (Build.VERSION.SDK_INT < 24) {
    // 低版本,直接获取图像保存路径
    imageUri = Uri.fromFile(outputImage);
} else {
    // 高版本创建一个内容提供器,供相机使用
    imageUri = FileProvider.getUriForFile(MainActivity.this, "com.example.cameraalbumtest.fileprovider", outputImage);
}

// 3. 启动相机程序, 并传入回调编号
Intent intent = new Intent("android.media.action.IMAGE_CAPTURE");
intent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri);
startActivityForResult(intent, TAKE_PHOTO);

// 4. 回调函数 
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {    
    if(requestCode == TAKE_PHOTO && resultCode == RESULT_OK ){
        // 5. 输出连接,得到图像文件 uri ,并返回流, 转为图像对象
        InputStream stream = getContentResolver().openInputStream(this.imageUri);
        Bitmap bitmap = BitmapFactory.decodeStream(stream);
        picture.setImageBitmap(bitmap);
    }
}

判断有没有相机

/**
 * 判断某个意图是否存在
 */
public static boolean isHaveCame(String intentName) {
    PackageManager packageManager = App.context.getPackageManager();
    Intent intent = new Intent(intentName);
    List<ResolveInfo> list = packageManager.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);
    return list.size() > 0;
}

相册选择照片

这里有一些说明 https://www.jianshu.com/p/7c6a53db8b12 https://link.jianshu.com/?t=https://github.com/YuanTiger/PhotoGet 里面有一段根据 兼容不同 uri 返回路径的代码

// 1. 判断并请求 Manifest.permission.WRITE_EXTERNAL_STORAGE 权限 
// 有权限时直接打开,没有权限 onRequestPermissionsResult 中进行回调用

// 2. 一个打开相册
Intent intent = new Intent("android.intent.action.GET_CONTENT");
intent.setType("image/*");
startActivityForResult(intent, CHOOSE_PHOTO); // 打开相册

// 3. 回调函数
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    if(requestCode == CHOOSE_PHOTO && resultCode == RESULT_OK ){
        // 判断手机系统版本号
        if (Build.VERSION.SDK_INT >= 19) {
            // 4.4及以上系统使用这个方法处理图片
            handleImageOnKitKat(data);
        } else {
            // 4.4以下系统使用这个方法处理图片
            handleImageBeforeKitKat(data);
        }
    }
}

// 4. 显示图像, TargetApi 见 TargetApi 节的说明 
// 这里很复杂。看起来是针对多种情况进行了兼容, 有需要再深入的进行分析 
@TargetApi(19)
private void handleImageOnKitKat(Intent data) {
        String imagePath = null;
        Uri uri = data.getData();
        Log.d("TAG", "handleImageOnKitKat: uri is " + uri);
        if (DocumentsContract.isDocumentUri(this, uri)) {
            // 如果是document类型的Uri,则通过document id处理
            String docId = DocumentsContract.getDocumentId(uri);
            if("com.android.providers.media.documents".equals(uri.getAuthority())) {
                String id = docId.split(":")[1]; // 解析出数字格式的id
                String selection = MediaStore.Images.Media._ID + "=" + id;
                imagePath = getImagePath(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, selection);
            } else if ("com.android.providers.downloads.documents".equals(uri.getAuthority())) {
                Uri contentUri = ContentUris.withAppendedId(Uri.parse("content://downloads/public_downloads"), Long.valueOf(docId));
                imagePath = getImagePath(contentUri, null);
            }
        } else if ("content".equalsIgnoreCase(uri.getScheme())) {
            // 如果是content类型的Uri,则使用普通方式处理
            imagePath = getImagePath(uri, null);
        } else if ("file".equalsIgnoreCase(uri.getScheme())) {
            // 如果是file类型的Uri,直接获取图片路径即可
            imagePath = uri.getPath();
        }
        displayImage(imagePath); // 根据图片路径显示图片
    }

// 早期版本中根据所有的数据直接读出图像
private void handleImageBeforeKitKat(Intent data) {
    Uri uri = data.getData();
    String imagePath = getImagePath(uri, null);
    displayImage(imagePath);
}

//  使用内容提供器读取文件
private String getImagePath(Uri uri, String selection) {
    String path = null;
    // 通过Uri和selection来获取真实的图片路径
    Cursor cursor = getContentResolver().query(uri, null, selection, null, null);
    if (cursor != null) {
        if (cursor.moveToFirst()) {
            path = cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media.DATA));
        }
        cursor.close();
    }
    return path;
}
// 显示图片文件
private void displayImage(String imagePath) {
    Bitmap bitmap = BitmapFactory.decodeFile(imagePath);
    picture.setImageBitmap(bitmap);
}

TargetApi

TargetApi 是为了在低版本的 sdk 中能调用高版本的 sdk 而声明的。 要使用高级版本的 sdk 有 3种方式。

  1. 将目标平台设置为高版本,但是在低版本的机器上运行的时候会死得很难看
  2. 将目标平台设置为低版本,然后通过反射获得高版本的 sdk 函数,然后再进行调用 。但是开发难度会很高
  3. 将目标平台设置为低版本,但在调用高版本的函数上声明 @TargetApi(版本号)。这样就不会发生编译错误了。但是在代码中根据版本判断调用 不同的函数即可。

  4. asf

  5. af

  6. asf

  7. saf

  8. asf

  9. asf

  10. sadf

  11. afasf

  12. asf