安卓学习笔记4——简易新闻客户端
一、项目知识点:
1、简易服务器搭建,内容准备
本文使用简单的Tomcat服务器,配置方法参考博文:
https://blog.csdn.net/qq_40881680/article/details/83582484#Tomcat
注意:
(1)安装JRE或者JDK,并在系统环境变量中加入JDK_HOME变量D:\Java\jdk1.8.0_231
(2)服务器启动,报错等详见该博文
安装后的效果:
cmd
Tomcat
打开本地服务器网页:localhost:8080
内容准备:将提前准备的图片和xml文件放置在webapps的root根目录下
安卓手机将要访问该news.xml文件,并根据该xml文件进行解析;获取图片地址,展示信息和图片,端口默认使用8080
电脑的IP通过cmd ipconfig查看:
2、Xml文件解析
(1)XML文件:
包含文件头(声明)、根节点、节点、内容、命名空间等内容
命名空间示意
(2)生成XML文件
-
自己组拼
-
使用Xml序列化器xmlserializer
(3)解析XML文件
(4)本文的news.xml文件:
<channel>
<item>
<title>军报评徐才厚</title>
<description>人死账不消反腐步不停,支持,威武,顶,有希望了。</description>
<image>http://192.168.1.127:8080/img/a.jpg</image>
<type>1</type>
<comment>163</comment>
</item>
<item>
<title>女司机翻车后直奔麻将室</title>
<description>女司机翻车后直奔麻将室,称大难不死手气必红</description>
<image>http://192.168.1.127:8080/img/b.jpg</image>
<type>2</type>
</item>
<item>
<title>小伙当“男公关”以为陪美女</title>
<description>来源:中国青年网,小伙当“男公关”以为陪美女,上工后被大妈吓怕</description>
<image>http://192.168.1.127:8080/img/c.jpg</image>
<type>3</type>
</item>
<item>
<title>男子看上女孩背影xxxx</title>
<description>来源:新京报,看到正脸后很惊喜</description>
<image>http://192.168.1.127:8080/img/d.jpg</image>
<type>1</type>
<comment>763</comment>
</item>
</channel>
3、ListView控件使用(Imageview,TextView)
ListView是可以垂直滚动的列表,可以展示多个条目
(1)ListView简单使用
- 定义Listview:在布局中定义一个LIstView;单独建立条目的布局文件
- 定义ListView的适配器
- 实现适配器中的方法getcount、getview等
(2)ListView优化
- Listview每次滑动到新的item,就会调用getview方法;如果不断创建view对象,会造成内存紧张;因此,通过converview实现复用:
(3)ListView显示数据原理
(4)ListView一些小问题
View的宽高属性使用:包裹内容;展示多个条目时无法自动计算应展示的条目数,安卓系统自动多次校验,效率降低
(5)ListView显示复杂页面
此处就是需要将item的布局文件转换为View对象,采用View.inflate方法
4、网络编程
(1)HttpUrlConnection
控制网络收发数据:
URL url=new URL("https://www.baidu.com");
// 创建httpurlconnection对象
HttpURLConnection conn= (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
conn.setConnectTimeout(5000);
int code =conn.getResponseCode();
if (code==200){
// 获取服务器返回的数据
InputStream in=conn.getInputStream();
// 把流装换为字符串,抽出为工具类
String content =StreamTools.readStream(in);
tv_result.setText(content);
}
} catch (Exception e) {
e.printStackTrace();
}
(2)注意:安卓4.0以后联网操作只能在子线程
在实验过程中,在主线程里进行网络连接,报错:
android.os.NetworkOnMainThreadException
- 主线程 :UI线程,onCreat方法所在的线程
- 其他耗时的操作(联网,拷贝文件等)需要放在子线程中,否则可能会报“应用无响应”错误:ANR
(3)注意:安卓9.0以后联网强制使用https协议
这在正常访问网络时没有什么区别,但是本教程中使用tomcat在本地搭建了一下服务器进行访问,由于网站没有进行安全备案,使用https协议时出现错误:
CLEARTEXT communication not permitted by network security policy
有的请求库不会抛出这个错误,如果应用无故连接超时,抛出SocketTimeoutException,很可能也是这个原因引起的
- 网上较简单的解决办法是通过在manifest的application标签中加入以下设置,来修改默认策略,这样就可以在安卓9.0中使用http协议
<application
android:usesCleartextTraffic="true"">
</application>
5、Handler:消息机制
(1)用途:
- 由于在子线程需要更新UI,(由于多个子线程之间的同步与互斥,比较复杂,google为了简化编程)安卓只允许在主线程中更新UI,因此采用消息机制
在子线程中更新UI:会报错:
W/System.err:> android.view.ViewRootImpl$CalledFromWrongThreadException:
网络连接
public class MainActivity extends AppCompatActivity {
protected static final int REQUESTSUCCESS=0;
protected static final int REQUESTNOTFOUND=1;
protected static final int REQUESTEXCEPTION=2;
private EditText et_path;
private TextView tv_result;
private Handler handler =new Handler(){
//这个方法在主线程中执行
@Override
public void handleMessage(@NonNull Message msg) {
switch (msg.what){
case REQUESTSUCCESS:
String content=(String) msg.obj;
tv_result.setText(content);
break;
case REQUESTNOTFOUND:
Toast.makeText(getApplicationContext(),"资源不存在",Toast.LENGTH_LONG).show();
break;
case REQUESTEXCEPTION:
Toast.makeText(getApplicationContext(),"请求异常",Toast.LENGTH_LONG).show();
break;
default:
break;
}
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
et_path =findViewById(R.id.et_path);
tv_result =findViewById(R.id.tv_result);
}
public void myclick(View V){
//创建子线程,在子线程中进行联网操作
new Thread(){
@Override
public void run() {
try {
String path=et_path.getText().toString().trim();
URL url=new URL("Https://"+path);
// 创建httpurlconnection对象
HttpURLConnection conn= (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
conn.setConnectTimeout(5000);
int code =conn.getResponseCode();
if (code==200){
// 获取服务器返回的数据
InputStream in=conn.getInputStream();
// 把流装换为字符串,抽出为工具类
String content =StreamTools.readStream(in);
// tv_result.setText(content);
// 更新UI的操作,使用Hander实现
Message msg=Message.abtain();//可以减少对象的创建
msg.what=REQUESTSUCCESS;
msg.obj=content;
handler.sendMessage(msg);
}
else{
// 请求不成功
Message msg=Message.abtain();//可以减少对象的创建
msg.what=REQUESTSUCCESS;
handler.sendMessage(msg);
}
} catch (Exception e) {
e.printStackTrace();
Message msg=Message.abtain();//可以减少对象的创建
msg.what=REQUESTEXCEPTION;
handler.sendMessage(msg);
}
}
}.start();
}
}
(2)消息机制使用步骤
- 在主线程定义一个Handler private Handler handler=new Handler()
- 重写handler的里面的handlemessage方法
public void handleMessage(android.os.Message msg){} - 拿着我们在主线程创建的handler去子线程发消息handler.sendMessage(msg);
- handlemessage方法就会执行在这个方法里面去更新UI
- mesg可以携带任何数据:当handler需要处理多条不同消息时,msg.what可用于区分不同消息类型
(3)消息机制原理
- 系统创建主线程时会创建消息循环器Looper
- Looper中包含一个消息队列MessageQueue; Looper不停的监视消息队列,并从中取出message对象,
- Looper.target为handler对象,Looper会调用handler的dispatchMessage方法,该方法会调用handleMessage方法;该方法就是我们创建handler对象时需要重写的方法、
(4)Handler其他API
- 延迟执行:Handler().postDelayed()
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
System.out.println("nihao");
}
},5000);
java中Timer可实现类似的功能(app退出后需要销毁Timer调度)
Timer timer=new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("hahhhahah");
}
}, 5000,);
但是Timer子线程中无法更新UI,但是Handler的postDelayed方法可以
6、图片显示
(1)Imgview控件显示图片
- 使用Bitmap显示图片:imgview对象setImageBitmap()方法
- 使用bitmapFactory. decodeStream(inputStream in)
将url打开的输入流转换为bitmap
(2)图片缓存
- 采用文件输出流,将url.openconnection()返回的输入流传入byte[] buffer后再写入文件
File file=new File(getCacheDir(), Base64.encodeToString(path.getBytes(),Base64.URL_SAFE));
InputStream in =conn.getInputStream();
//缓存数据
FileOutputStream fos=new FileOutputStream(file);
int len=-1;
byte[] buffer=new byte[1024];//1kb
while((len=in.read(buffer))!=-1){
fos.write(buffer,0,len);
}
fos.close();
- 需要注意的是:new FileOutputStream(file)时file采用base64编码不会对"/"编码,因此可能导致无法找到文件;此时采用Base64.URL_SAFE选项
- 缓存逻辑:
- 首次访问该图片根据path地址进行base64编码,将编码后的字符串作为文件名,将数据存入该文件中
- 非首次访问该path,则直接看base64编码文件名的文件是否存在,存在则直接从文件获得bitmap
总代码:
public class MainActivity extends AppCompatActivity {
private TextView et_path;
private ImageView iv;
private Handler handler=new Handler(){
@Override
public void handleMessage(@NonNull Message msg) {
iv.setImageBitmap((Bitmap)msg.obj);
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
et_path=(TextView) findViewById(R.id.et_path);
iv=(ImageView) findViewById(R.id.iv);
}
public void myclick(View v){
new Thread(){
@Override
public void run() {
try {
String path=et_path.getText().toString().trim();
File file=new File(getCacheDir(), Base64.encodeToString(path.getBytes(),Base64.URL_SAFE));
if (file.exists() && file.length()>0){
// 如果文件存在,使用缓存数据
Bitmap bitmap= BitmapFactory.decodeFile(file.getAbsolutePath());
Message msg=Message.obtain();
msg.obj=bitmap;
handler.sendMessage(msg);
}
else{
// 如果文件不存在,则连接网络
URL url=new URL(path);
HttpURLConnection conn=(HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
conn.setConnectTimeout(5000);
int code=conn.getResponseCode();
//获取图片数据,
if (code==200){
InputStream in =conn.getInputStream();
//缓存数据
FileOutputStream fos=new FileOutputStream(file);
int len=-1;
byte[] buffer=new byte[1024];//1kb
while((len=in.read(buffer))!=-1){
fos.write(buffer,0,len);
}
fos.close();
in.close();
//将输入流转换为bitmap模式,BitmapFactory从各种资源
Bitmap bitmap= BitmapFactory.decodeFile(file.getAbsolutePath());
Message msg=Message.obtain();
msg.obj=bitmap;
handler.sendMessage(msg);
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}.start();
}
}
(3)关于缓存目录
- getCacheDir:在data/user/0/应用文件夹/cache:一般用来放不重要的缓存数据,系统有清理缓存的功能
- getFileDir:重要的数据信息
(4)开源项目SmartImageView
使用开源项目SmartImageView可以简化图片显示的代码,在本项目中使用了根据图片URL地址显示图片的方法
SmartImageView源码:https://github.com/loopj/android-smart-image-view
使用步骤:
- 下载源码,将loopj文件夹拷贝到源码com路径下
- 将原来布局文件的imageView改为com.loopj.android.image.SmartImageView(完整引用)
- 代码中使用到该ImageView中的地方,改用SmartImageView
SmartImageView iv_icon=(SmartImageView) view.findViewById(R.id.iv_icon);
String imageUrl =newsList.get(position).getImage();
iv_icon.setImageUrl(imageUrl);
7、更新UI的便捷API:runOnUiThread
由于在子线程里处理数据,然后更新UI——是十分常见的操作,因此提供了runOnUiThread()方法;其本质还是采用handler实现
使用方法:在子线程中直接使用runOnUiThread()方法,传入一个Runnable对象
以下代码:
Bitmap bitmap= BitmapFactory.decodeFile(file.getAbsolutePath());
Message msg=Message.obtain();
msg.obj=bitmap;
handler.sendMessage(msg);
可以替换为:
final Bitmap bitmap= BitmapFactory.decodeFile(file.getAbsolutePath());
runOnUiThread(new Runnable() {
@Override
public void run() {
iv.setImageBitmap(bitmap);
}
});
二、项目实战:
1、功能:
- 实现通过服务器地址取数据(xml文件);根据文件中包含的文字信息和图片源信息;拿到真实的数据;并展示
2、几个注意的点:
- 更新UI在在主线程,耗时操作在子线程
- 安卓9.0使用Http协议如何设置
- 总体布局文件中嵌套item布局
- ListView如何适配
- SmartImageView如何使用及原理