安卓學習筆記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如何使用及原理