先来个pull解析测试,然后是pull解析用法说明,文章末尾附有xml生成方式。
学习目标:首先是解析测试例子给出的对于常用字段的理解,然后是pull解析常用套路方法,最后是xml的2种生成方式。
经常写代码需要申请动态权限,在最后例子也顺带记录下来,方便查阅。
目录
pull解析例子的源码:https://github.com/liuchenyang0515/PullDemo
xml生成方式的源码:https://github.com/liuchenyang0515/SaveXmlInfo
关于正常权限、危险权限及权限组的介绍见官方文档:https://developer.android.google.cn/guide/topics/security/permissions?utm_source=udacity&utm_medium=course&utm_campaign=android_basics#normal-dangerous
pull解析测试:
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.util.Xml;
import android.view.View;
import org.xmlpull.v1.XmlPullParser;
import java.io.InputStream;
public class MainActivity extends AppCompatActivity {
private String TAG = "MainActivity";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
public void onclick(View view) {
try {
InputStream is = getAssets().open("info.xml");
// 解析info.xml文件
// 1.得到xml文件的解析器
XmlPullParser parser = Xml.newPullParser();
// 2.设置输入流和编码
parser.setInput(is, "utf-8");
// 3.解析xml文件,获取当前的事件类型
int type = parser.getEventType();
while (type != XmlPullParser.END_DOCUMENT){
// getText()是获取内容,整个流程相当于一个指针指一次开始标签再指一次内容,
// (内容的getName()为null, getText()才是取内容字符串,如果没内容就是"")
// 再指一次结束标签(如果没遇到结束标签就指向下一个开始标签),然后再次指向内容,如果没有getName()就是null
// 否则就指向下一个标签,依次重复这个过程
// 经测试,事件类型依次为
// 最顶端document 0
// 开始标签 2
// 内容 4
// 结束标签 3
Log.d(TAG, parser.getEventType() + "..." + parser.getName() + "..." + parser.getText());
type = parser.next();
}
is.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
常用的字段:
int START_DOCUMENT = 0;
int END_DOCUMENT = 1;
int START_TAG = 2;
int END_TAG = 3;
int TEXT = 4;
笔记批注:
getText()是获取内容,整个流程相当于一个指针指一次开始标签再指一次内容,再指一次结束标签(如果没遇到结束标签就指向下一个开始标签),然后再次指向内容。
标签只有getName(),而getText()为null, 内容只有getText(),而getName()为null。getText()取字符串,如果字符串就是空串"",说明本行后面没内容了,那么就开始进行下一行的解析。
比如获取开始标签的下一次没有内容而是另一个开始标签,那么getText()是"",进行下一行解析,或者遇到结束标签的下一次获取内容getText()是"",说明后面没内容了,进行下一行解析。
如果getName()没有,值就是null(比如TEXT字段)
如下图。
即不管是开始还是结束标签,只要遇到标签,下一次就会尝试获取内容,getEventType()得到了START_DOCUMENT和END_TAG字段, 那么下一次getEventType()一定是TEXT字段。
SAX解析和PULL解析原理是一样的,可以见我另一篇博客之中写到的SAX解析:SAX解析代码原理分析
xml如下:
运行结果如下:
pull解析例子:
MainActivity.java:
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.widget.TextView;
import java.io.InputStream;
import java.util.List;
public class MainActivity extends AppCompatActivity {
private String TAG = "MainActivity";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 1.显示天气信息
TextView tv_weather = (TextView) findViewById(R.id.tv_weather);
try ( // 1.1获取资产的管理者,通过上下文
InputStream inputStream = getAssets().open("weather.xml");) {
// 2.调用我们定义的解析xml业务方法
List<Channel> list = Weather.parserXml(inputStream);
StringBuffer sb = new StringBuffer();
for (Channel channel : list) {
sb.append(channel.toString() + "
");
}
// 3.把数据展示到textview
tv_weather.setText(sb);
} catch (Exception e) {
e.printStackTrace();
}
}
}
Weather.java:
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserFactory;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
public class Weather {
/**
* 服务器是以流的形式把数据返回的
*/
public static List<Channel> parserXml(InputStream in) throws Exception {
List<Channel> weatherLists = null;
Channel channel = null;
// 1.获取XmlPullParser解析的实例
// 简单工厂方法的例子
XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
XmlPullParser parser = factory.newPullParser();
// 也可以直接一步写XmlPullParser parser = Xml.newPullParser();
// 通过Xml.newPullParser()获得的解析器可能会有一个bug:调用nextText()并不总是前进到END_TAG
// 一些app可能围绕着这个问题,额外的调用next()或nextTag()方法:
// 在Android Ice Cream Sandwich版本中,删除了ExpatPullParser类来修复这个bug,
// 不幸的是,app在Android4.0版本下使用它可能会导致应用crash
// 官方说明文档是用的2步,如我上面写的,就当做是推荐这种写法吧
// 2.设置XmlPullParser参数
parser.setInput(in, "utf-8");
// 3.获取事件类型
int type = parser.getEventType();
while (type != XmlPullParser.END_DOCUMENT) {
String nodeName = parser.getName();
// 4.具体判断一下解析到哪里了
switch (type) {
case XmlPullParser.START_TAG:
if ("weather".equals(nodeName)) {
// 5.创建一个集合对象
weatherLists = new ArrayList<Channel>();
} else if ("channel".equals(nodeName)) {
// 6.创建Channel对象
channel = new Channel();
// 7.获取id值
// 也可以parser.getAttributeValue(null, "id");
String id = parser.getAttributeValue(0); // 0号属性
channel.setId(id);
} else if ("city".equals(nodeName)) {
// 8.获取city的数据
String temp = parser.nextText();// 刚刚测试过了getText()是获得本次指向的内容
// 这里nextText()就是指向下一次指向的内容,即开始和结束标签之间的内容
channel.setTemp(temp);
} else if ("wind".equals(nodeName)) {
// 9.获取wind数据
String wind = parser.nextText();
channel.setWind(wind);
} else if ("pm250".equals(nodeName)) {
// 9.获取wind数据
String pm250 = parser.nextText();
channel.setPm250(pm250);
}
break;
case XmlPullParser.END_TAG:
if ("channel".equals(nodeName)) { // 已经测试过了,结束标签不会读取前面的/
// 把javabean对象存到集合中
weatherLists.add(channel);
}
break;
}
// 不停的向下解析
type = parser.next();
}
return weatherLists;
}
}
笔记批注:
// 1.获取XmlPullParser解析的实例
XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
XmlPullParser parser = factory.newPullParser();
这个就是简单工厂方法模式的一个例子。
也可以直接一步写XmlPullParser parser = Xml.newPullParser(); 通过Xml.newPullParser()获得的解析器可能会有一个bug:调用nextText()并不总是前进到END_TAG一些app可能围绕着这个问题,额外的调用next()或nextTag()方法:在Android Ice Cream Sandwich版本中,删除了ExpatPullParser类来修复这个bug,不幸的是,app在Android4.0版本下使用它可能会导致应用crash, 官方说明文档是用的2步,如我上面写的,就当做是推荐这种写法吧
详情参考博客:https://blog.csdn.net/u013656135/article/details/49840125
关于方法使用:
getAttributeValue(int index);//大意就是返回指定位置的属性值,位置从0开始
getAttributeValue(String namespace,String name); // 大意就是返回指定的属性名对应的属性值,如果没有使用命名空间,则第一个参数传入null,第二个参数是属性名,这个例子是"id"属性
Channel.java:
public class Channel {
private String id;
private String city;
private String temp;
private String wind;
private String pm250;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getCity() {
return city;
}
public void setCity(String city) {
this.city = city;
}
public String getTemp() {
return temp;
}
public void setTemp(String temp) {
this.temp = temp;
}
public String getWind() {
return wind;
}
public void setWind(String wind) {
this.wind = wind;
}
public String getPm250() {
return pm250;
}
public void setPm250(String pm250) {
this.pm250 = pm250;
}
@Override
public String toString() {
return "Channel [id=" + id + ", city=" + city + ", temp=" + temp
+ ", wind=" + wind + ", pm250=" + pm250 + "]";
}
}
assets目录下的weather.xml:
<?xml version="1.0" encoding="utf-8"?>
<weather>
<channel id='1'>
<city>北京</city>
<temp>25°</temp>
<wind>3</wind>
<pm250>300</pm250>
</channel>
<channel id='2'>
<city>郑州</city>
<temp>20°</temp>
<wind>4</wind>
<pm250>300</pm250>
</channel>
<channel id='3'>
<city>长春</city>
<temp>10°</temp>
<wind>4</wind>
<pm250>100</pm250>
</channel>
<channel id='4'>
<city>沈阳</city>
<temp>20°</temp>
<wind>1</wind>
<pm250>50</pm250>
</channel>
</weather>
笔记批注:
assets与res/raw不同:
assets目录是Android的一种特殊目录,用于放置APP所需的固定文件,且该文件被打包到APK中时,不会被编码到二进制文件。
Android还存在一种放置在res下的raw目录,该目录与assets目录不同。
注意点:
1、 assets目录不会被映射到R中,因此,资源无法通过R.id方式获取,必须要通过AssetManager进行操作与获取;res/raw目录下的资源会被映射到R中,可以通过getResource()方法获取资源。
2、 多级目录:assets下可以有多级目录,res/raw下不可以有多级目录。
3、 编码(都不会被编码):assets目录下资源不会被二进制编码;res/raw应该也不会被编码。
运行结果如下:
xml生成方式(代码添加申请权限示范模版):
本demo的xml是一个Button,属性添加android:onClick="save",然后几个编辑框提交。如下图:
SaveXmlInfo的Demo主要代码如下:
import android.Manifest;
import android.content.DialogInterface;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.os.Environment;
import android.support.annotation.NonNull;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;
import android.support.v7.app.AlertDialog;
import android.support.v7.app.AppCompatActivity;
import android.text.TextUtils;
import android.util.Xml;
import android.view.View;
import android.widget.EditText;
import android.widget.Toast;
import org.xmlpull.v1.XmlSerializer;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.util.ArrayList;
import java.util.List;
public class MainActivity extends AppCompatActivity {
private EditText et_name, et_age, et_id;
private final int MY_PER_CODE = 15;
private String name, age, id;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
et_name = (EditText) findViewById(R.id.et_name);
et_age = (EditText) findViewById(R.id.et_age);
et_id = (EditText) findViewById(R.id.et_id);
}
public void save(View view) {
name = et_name.getText().toString().trim();
age = et_age.getText().toString().trim();
id = et_id.getText().toString().trim();
if (TextUtils.isEmpty(name) || TextUtils.isEmpty(age) || TextUtils.isEmpty(id)) {
Toast.makeText(this, "信息不能为空", Toast.LENGTH_SHORT).show();
return;
} else {
List<String> permissionList = new ArrayList<>();
if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)
!= PackageManager.PERMISSION_GRANTED) {
if (!permissionList.contains(Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
permissionList.add(Manifest.permission.WRITE_EXTERNAL_STORAGE);
}
}
if (!permissionList.isEmpty()) {
String[] permissions = permissionList.toArray(new String[permissionList.size()]);
ActivityCompat.requestPermissions(this, permissions, MY_PER_CODE);
} else {
writeXml_1();
//writeXml_2();
}
}
}
private void writeXml_2() {
// 1.创建一个xml文件的序列化器
XmlSerializer serializer = Xml.newSerializer();
try (FileOutputStream fos = new FileOutputStream(new File
(Environment.getExternalStorageDirectory(), "info1.xml"));) {
// 设置文件的输出和编码方式
// 设置为使用具有给定编码的二进制输出流。
serializer.setOutput(fos, "utf-8");
// 写xml文件的头
// 使用编码(if encoding not null)和独立标志(if standalone not null)编写<?xml声明,此方法只能在setOutput之后调用。
serializer.startDocument("utf-8", true);
// 4.写info结点
// 使用给定的命名空间和名称写入开始标记。如果没有为给定的命名空间定义前缀,则将自动定义前缀。
// 如果名称空间为NULL,则不打印名称空间前缀,而只打印名称。
serializer.startTag(null, "info");
// 5.写student节点
serializer.startTag(null, "student");
// 6.写属性
// 写一个属性。对Attribute()的调用必须立即跟随对startTag()的调用。
serializer.attribute(null, "id", id);
// 7.写name
serializer.startTag(null, "name");
serializer.text(name);
serializer.endTag(null, "name");
// 8.写age
serializer.startTag(null, "age");
serializer.text(age);
serializer.endTag(null, "age");
serializer.endTag(null, "student");
serializer.endTag(null, "info");
// 写完。所有未关闭的开始标记将被关闭,输出将被刷新。在调用此方法之后,在下次调用setOutput()之前,不能序列化更多的输出。
serializer.endDocument();
Toast.makeText(this, "保存学生信息成功", Toast.LENGTH_SHORT).show();
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
switch (requestCode) {
case MY_PER_CODE:
if (grantResults.length > 0) {
for (int result : grantResults) {
if (result != PackageManager.PERMISSION_GRANTED) { // 如果用户不同意
// ActivityCompat.shouldShowRequestPermissionRationale用法见下面笔记批注
if (ActivityCompat.shouldShowRequestPermissionRationale(this,
Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
showPermissionDialog(permissions);
} else { // 不同意的情况下还勾选了“不再提醒”
Toast.makeText(MainActivity.this, "您已拒绝权限,请在设置手动打开", Toast.LENGTH_SHORT).show();
}
return; // 用户不同意的情况下,到这里直接返回
}
}
writeXml_1();
//writeXml_2();
} else {
Toast.makeText(this, "未知错误!", Toast.LENGTH_SHORT).show();
finish();
}
break;
default:
break;
}
}
private void showPermissionDialog(final String[] permissions) {
AlertDialog.Builder dialog = new AlertDialog.Builder(this);
dialog.setTitle("提示!");
dialog.setMessage("这个权限关系到功能使用,如拒绝需要在设置手动打开!");
dialog.setCancelable(false); // 点击空白处不可取消
dialog.setPositiveButton("授权", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
ActivityCompat.requestPermissions(MainActivity.this, permissions, MY_PER_CODE);
}
});
dialog.setNegativeButton("拒绝", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
}
});
dialog.show();
}
private void writeXml_1() {
StringBuffer sb = new StringBuffer();
sb.append("<?xml version='1.0' encoding='utf-8' standalone='yes' ?>");
sb.append("<info>");
sb.append("<student id="" + id + "">");
sb.append("<name>" + name + "</name>");
sb.append("<age>" + age + "</age>");
sb.append("</student>");
sb.append("</info>");
try (BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(
new FileOutputStream(new File(Environment.getExternalStorageDirectory(), "info.xml"))));) {
bw.write(sb.toString());
Toast.makeText(this, "保存学生信息成功", Toast.LENGTH_SHORT).show();
} catch (IOException e) {
e.printStackTrace();
Toast.makeText(this, "保存学生信息失败", Toast.LENGTH_SHORT).show();
}
}
}
writeXml_1()是通过文件写xml,writeXml_2()是通过序列化器写xml。
笔记批注:
ActivityCompat.shouldShowRequestPermissionRationale用法:
应用安装后第一次访问,如果未开始获取权限申请直接返回false;可能此时并未请求权限而执行到此方法
第一次请求权限时,用户拒绝了,下一次shouldShowRequestPermissionRationale()返回 true,这时候可以显示一些为什么需要这个权限的说明;
第二次以及之后请求权限时,用户拒绝了,并选择了“不再提醒”的选项时:shouldShowRequestPermissionRationale()返回 false;
第二次以及之后请求权限时,用户拒绝了,但没有勾选“不再提醒”选项,返回true,继续提醒
设备的系统设置中禁止当前应用获取这个权限的授权,shouldShowRequestPermissionRationale()返回false;
=================================Talk is cheap, show me the code=================================