感冒了幾天,今天總算正常了點,想了想我還有未完成的最後一類博文,馬上就跳起來,接着奮鬥了。廢話就不多說了先上圖看看網易新聞APP的标題欄24小時要聞如下:
![](https://img.laitimes.com/img/_0nNw4CM6IyYiwiM6ICdiwiIyVGduV2QvwVe0lmdhJ3ZvwFM38CXlZHbvN3cpR2Lc1TPB10QGtWUCpEMJ9CXsxWam9CXwADNvwVZ6l2c052bm9CXUJDT1wkNhVzLcRnbvZ2LcZXUYpVd1kmYr50MZV3YyI2cKJDT29GRjBjUIF2LcRHelR3LcJzLctmch1mclRXY39TOyYTM1UTMwIjNyATM1EDMy8CX0Vmbu4GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.jpg)
我們即将完成的效果圖:
圖檔顯示0是預設圖檔,代表該新聞沒有圖檔,不過,該程式員實作的功能我都實作了,這裡面涉及的知識有平移動畫,網絡爬蟲,漢字截取算法,各個編碼中漢字占多少位元組,異步加載(及自定義ListView添加頭部加載特效及分頁顯示),這裡為什麼加個括号,因為這個功能不是本片文章所講解的,一篇文章将前面那些功能已經夠多了,等下下篇文章講解完橫向滑動菜單後,就會講解listView設定的特效。
首先知識點得慢慢講解,一下講解篇幅太大無法講解完全,是以,本文代碼的漏洞,會在優化ListView中改進,就像Android程式設計權威指南一樣,随着深入,慢慢優化。
1.标題欄布局檔案
首先,我們來看看網易的标題欄效果圖:
當然背景是紅色,那麼,請根據上篇文章在反編譯的檔案夾中找到這三個圖檔,下面是整個标題欄的布局檔案lyj_title_bar_layout.xml代碼:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/lyj_title_bar_layout_main"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#EB413D">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp">
<ImageView
android:layout_width="57dp"
android:layout_height="30dp"
android:layout_marginLeft="8dp"
android:background="@drawable/base_common_default_icon_small"
android:scaleType="fitXY" />
<ImageButton
android:id="@+id/importantNews"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_toLeftOf="@+id/weather"
android:background="@drawable/ic_msg_center_header"
android:scaleType="fitXY" />
<ImageButton
android:id="@+id/weather"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_alignParentRight="true"
android:background="@drawable/ic_newspage_menu_moreoverflow"
android:scaleType="fitXY" />
</RelativeLayout>
</LinearLayout>
這個相信不需要解釋很多,一般都會懂得。
将此布局檔案寫進NewsFragment類的布局檔案news_fragment_main_layout.xml中,代碼如下:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<include layout="@layout/lyj_title_bar_layout"/>
</RelativeLayout>
運作後,将會得到如下圖所示的效果:
雖然沒有網易那麼美,但是基本就是這樣,裡面的圖檔太多,我隻找了幾個很像的圖檔。
2.24小時要聞布局檔案
下面是分析24小時要聞布局的截圖:
important_news_layout.xml代碼如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/important_news_layout_mymain"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:visibility="gone">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="200dp"
android:background="@drawable/biz_msg_center_header_day_bg"
android:orientation="vertical">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal">
<ImageButton
android:id="@+id/important_news_layout_back_but"
android:layout_width="32dp"
android:layout_height="32dp"
android:background="@drawable/abc_ic_ab_back_mtrl_am_alpha" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="5dp"
android:layout_marginTop="5dp"
android:text="@string/important_news_layout_24txt"
android:textColor="@android:color/white"
android:textSize="18sp" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<ImageView
android:layout_width="72dp"
android:layout_height="72dp"
android:layout_gravity="center"
android:layout_marginTop="50dp"
android:background="@drawable/ic_msg_center_header" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="@string/important_news_layout_txt"
android:textColor="@android:color/white"
android:textSize="15sp" />
</LinearLayout>
</LinearLayout>
<ListView
android:id="@+id/important_news_layout_listview"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
同樣是布局檔案,大同小異而已。不過此布局檔案預設是隐藏的,是以設定為GONE。
将此布局檔案添加自NewsFragment的布局檔案news_fragment_main_layout.xml中,代碼如下:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<include layout="@layout/lyj_title_bar_layout"/>
<include layout="@layout/important_news_layout"/>
</RelativeLayout>
3.平移動畫
打開網易,點選24小時要聞,你會發現,此界面是從右向左滑動出來的,也就是平移出來的,這就是平移動畫,那麼動畫有兩種實作方式:
①通過引用XML檔案裡面已經寫好的動畫,實作動畫效果。
②動态的使用代碼寫出動畫。
顯然,通過XML寫動畫,即簡潔又美觀。
我們通過XML寫的動畫同意放在res/anim檔案夾下,下面的代碼就是我們24小時要聞需要使用到的平移動畫important_news_layout_show_anim.xml:
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate
android:duration="500"
android:fromXDelta="100%"
android:toXDelta="0%"
android:fillAfter="true"/>
</set>
這個是滑動出來的代碼,當然也有滑動消失的代碼important_news_layout_hide_anim.xml:
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate
android:duration="500"
android:fromXDelta="0%"
android:toXDelta="100%"
android:fillAfter="true"/>
</set>
我們來一條一條解釋:
translate:就是平移動畫,也是平移動畫屬性的父标簽。
android:duration="500":動畫執行時間半秒,一秒為1000。
android:fillAfter="true":動畫結束時畫面停留在最後一幀。
android:fromXDelta="100%",android:toXDelta="0%":以百分比表示,就是從自己X的100%,也就是布局的寬度,也就是右邊界,X的0%,也就是左邊界,這就是右平移到左。
同理android:fromXDelta="0%",android:toXDelta="100%":以百分比表示,就是從自己X的0%,也就是布局的寬度,也就是左邊界,X的100%,也就是左邊界,這就是左平移到右。
Y沒寫意味着不變。
NewsFragment.class執行動畫的代碼如下:
public class NewFragment extends Fragment {
//24小時要聞代碼片段
private LinearLayout importLayout;//整個24小時要聞的布局檔案
private ImageButton importNews;//打開24小時新聞的按鈕
private ImageButton importBackBut;//關閉24小時新聞的回退按鈕
private ListView importListView;//24小時新聞的listView
private ImportantNewsAdapter importAdapter;//listView對應的adapter
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view=inflater.inflate(R.layout.news_fragment_main_layout,container,false);
//24小時要聞代碼片段
this.importLayout=(LinearLayout)view.findViewById(R.id.important_news_layout_mymain);
this.importNews=(ImageButton)view.findViewById(R.id.importantNews);
this.importBackBut=(ImageButton)view.findViewById(R.id.important_news_layout_back_but);
this.importListView=(ListView)view.findViewById(R.id.important_news_layout_listview);
final Animation translateShow=AnimationUtils.loadAnimation(getActivity(), R.anim.important_news_layout_show_anim);//擷取平移顯示24小時新聞動畫
final Animation translateHide=AnimationUtils.loadAnimation(getActivity(), R.anim.important_news_layout_hide_anim);//擷取平移隐藏24小時新聞動畫
//24小時新聞顯示按鈕
this.importNews.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
importLayout.setAnimation(translateShow);//設定動畫
translateShow.start();//執行動畫
}
});
//24小時新聞隐藏按鈕
this.importBackBut.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
importLayout.setAnimation(translateHide);//設定動畫
translateHide.start();//執行動畫
importLayout.setVisibility(View.GONE);//設定隐藏
importAdapter=null;
}
});
return view;
}
}
寫在XML之中的動畫動過AnimationUtils.loadAnimation方法加載。這裡有個動畫漏洞,不過在這裡不會顯示出來,下篇講解标題欄之天氣按鈕會具體講解到,我的博文實戰類篇就是這樣,跟着開發進度講解知識點,APP用到的知識都會講,但講解會跟着開發進度講,開發到哪,講到哪,沒有碰到時候的不會多講。後續都會講到。
運作之後,你會看到你的動畫效果已經和網易的一模一樣了,隻是這個時候出來的隻有24小時新聞的上面部分,listview沒有資料是以沒有顯示。下面來擷取新聞。
4.改進的網絡新聞爬蟲
前面第一篇文章基本講解的是怎麼擷取PC新聞條目,手機端新聞詳情。為了統一擷取位址,我們擷取網易的手機新聞首頁,也就是http://news.163.com/mobile/,網易新聞用戶端擷取的就是該位址的新聞條目。
下面是改進後的網絡爬蟲程式代碼:
public class LYJJsoupWangYiTUtils {
/***
* 擷取新聞詳細文章
*
* @param url
* @return
* @throws Exception
*/
public static String jsoupNewsPage(String url) throws Exception {
String page = null;
byte[] htmlbyte = ConnectNetwork.getImageDat(url);
String html = new String(htmlbyte, "UTF-8");
Document doc = Jsoup.parse(html);
Element div = doc.select("div.article-body").get(0);
page = div.html().toString();
return page;
}
/**
* 擷取文章描述和文章圖檔
*
* @param messageItem
* @param aHref
* @throws Exception
*/
public static void jsoupNewsContentAndImage(MessageItem messageItem, String aHref) throws Exception {
String html = new String(ConnectNetwork.getImageDat(aHref), "UTF-8");
Document doc = Jsoup.parse(html);
Elements divs = doc.select("div.article-body");
if (divs.size() == 0) {
System.out.println(aHref);
return;
}
Element div = divs.first();
Elements ps = div.getElementsByTag("p");
if (ps.size() == 0) {
return;
}
Element p = null;
if (ps.size() <= 2) {
p = ps.first();
} else {
for (int i = 0; i < ps.size(); i++) {
if (ps.get(i).text().length() > 90) {
p = ps.get(i);
break;
} else if (ps.get(i).text().length() > 0) {
p = ps.get(i);
}
}
}
messageItem.setContent(p.text());
Elements imgs = div.getElementsByTag("img");
if (imgs.size() == 0) {
return;
} else {
String imgSrc = imgs.first().attr("src");
messageItem.setImageSrc(ConnectNetwork.getImageDat(imgSrc));
}
}
/**
* 加載24小時要聞
*
* @param url
* @return
* @throws Exception
*/
public static List<MessageItem> jsoupImportantNews(String url) throws Exception {
List<MessageItem> messageItemList = new ArrayList<>();
byte[] htmlbyte = ConnectNetwork.getImageDat(url);
String html = new String(htmlbyte, "GBK");
Document doc = Jsoup.parse(html);
Element sectionLabels = doc.select("section.ns-wnews").select("section.ns-mb40").get(1);
Elements aLabels = sectionLabels.select("a");
for (int i = 0; i < aLabels.size(); i++) {
MessageItem item = new MessageItem();
Element aLabel = aLabels.get(i);
item.setTitle(aLabel.text());
item.setHrefSrc(aLabel.attr("href"));
jsoupNewsContentAndImage(item, aLabel.attr("href"));
if (item.getContent() != null) {
messageItemList.add(item);
}
}
return messageItemList;
}
}
開始的文章已經詳細的解釋過了,這裡就不在闡述了。不過還是要提醒一下,我們擷取的是這裡的新聞:
因為我們我們優化ListView會在講解完橫向滑動菜單後講解,是以這裡,隻擷取了幾條新聞,等優化ListView後我們回來重新寫代碼的。因為優化ListView是一章文章的篇幅,必須單獨出來,否則一篇博文内容太多,會降低品質。
5.ListView擴充卡
為了友善擴充,我們選擇最簡單的也是最複雜的BaseAdapter。我們可以看到,新聞的第一張圖檔,與下面的不是一個格式,一個是垂直模式,一個是水準模式,是以我們在布局檔案中,将顯示内容的三個控件,放在了一個LinearLayout中,便于控制顯示方式。important_listview_item.xml代碼如下:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:id="@+id/important_listview_item_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<ImageView
android:id="@+id/important_listview_item_image"
android:layout_width="80dp"
android:layout_height="80dp"
android:scaleType="fitXY"/>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/important_listview_item_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<TextView
android:id="@+id/important_listview_item_description"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
</LinearLayout>
<ImageView
android:id="@+id/important_listview_item_topleftbg"
android:layout_width="18dp"
android:layout_height="18dp"
android:scaleType="fitXY"
android:background="@drawable/biz_msg_center_flag_bg"/>
<TextView
android:id="@+id/important_listview_item_number"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="13dp"
android:textColor="@android:color/white"/>
</RelativeLayout>
下面是繼承擴充卡BaseAdapter的ImportantNewsAdapter代碼:
public class ImportantNewsAdapter extends BaseAdapter{
private ArrayList<SoftReference<Bitmap>> mBitmapRefs = new ArrayList<SoftReference<Bitmap>>();
private List<MessageItem> messageItemList = new ArrayList<>();
private Context context;
public ImportantNewsAdapter(List<MessageItem> items, Context context) {
this.context=context;
this.messageItemList=items;
}
@Override
public int getCount() {
return messageItemList.size();
}
@Override
public Object getItem(int position) {
return messageItemList.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder viewHolder;
View view = convertView;
if (view == null) {
view = LayoutInflater.from(context).inflate(R.layout.important_listview_item, null);
viewHolder=new ViewHolder(view);
view.setTag(viewHolder);
} else {
viewHolder=(ViewHolder)view.getTag();
}
MessageItem messageItem=messageItemList.get(position);
if(position==0){//當第一條資料的時候,布局格式
viewHolder.layout.setOrientation(LinearLayout.VERTICAL);
LinearLayout.LayoutParams params=(LinearLayout.LayoutParams)viewHolder.imageView.getLayoutParams();
params.height=ApplyUtils.dip2px(context,200);
params.width=LinearLayout.LayoutParams.MATCH_PARENT;
params.setMargins(0,20,0,0);
viewHolder.imageView.setLayoutParams(params);
viewHolder.topLeftBg.setImageResource(R.drawable.biz_msg_center_flag_first_bg);
}else{//其他時候的布局格式
viewHolder.layout.setOrientation(LinearLayout.HORIZONTAL);
LinearLayout.LayoutParams params=(LinearLayout.LayoutParams)viewHolder.imageView.getLayoutParams();
params.height=ApplyUtils.dip2px(context,80);
params.width=ApplyUtils.dip2px(context,80);
viewHolder.imageView.setLayoutParams(params);
viewHolder.topLeftBg.setImageResource(R.drawable.biz_msg_center_flag_bg);
}
if(messageItem.getImageSrc()!=null){
ByteArrayInputStream bais=new ByteArrayInputStream(messageItem.getImageSrc());
BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = 2;
Bitmap bitmap= BitmapFactory.decodeStream(bais,null,options);
mBitmapRefs.add(new SoftReference<Bitmap>(bitmap));
viewHolder.imageView.setImageBitmap(bitmap);
}else{
viewHolder.imageView.setImageResource(R.drawable.biz_more_menu_t0);//當該文章沒有圖檔的時候的預設圖檔,是以你才會看到,文章開頭的那個0。
}
viewHolder.numberTxt.setText(String.valueOf(position+1));
viewHolder.titleTxt.setText(messageItem.getTitle());
try {
viewHolder.contentTxt.setText(ApplyUtils.substring(messageItem.getContent(),90)+"...");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return view;
}
class ViewHolder {
private ImageView imageView;
private TextView numberTxt;
private TextView titleTxt;
private TextView contentTxt;
private LinearLayout layout;
private ImageView topLeftBg;
ViewHolder(View view) {
this.imageView = (ImageView) view.findViewById(R.id.important_listview_item_image);
this.titleTxt = (TextView) view.findViewById(R.id.important_listview_item_title);
this.contentTxt = (TextView) view.findViewById(R.id.important_listview_item_description);
this.numberTxt = (TextView) view.findViewById(R.id.important_listview_item_number);
this.layout = (LinearLayout) view.findViewById(R.id.important_listview_item_layout);
this.topLeftBg = (ImageView) view.findViewById(R.id.important_listview_item_topleftbg);
}
}
}
這是ListView的通用模式,不畢過多解釋,這裡用到的軟引用,不過我并沒有完善它,它将會在優化ListView章節講解到,如何配合引用隊列使用它,這裡跳過,你這裡用不用都沒有多大關系。
需要解釋了還有以下幾點:
①.漢字截取
漢字和字母符号在如下編碼中,占多少位元組:
英文字母:A
位元組數:1;編碼:GB2312
位元組數:1;編碼:GBK
位元組數:1;編碼:GB18030
位元組數:1;編碼:ISO-8859-1
位元組數:1;編碼:UTF-8
位元組數:4;編碼:UTF-16
位元組數:2;編碼:UTF-16BE
位元組數:2;編碼:UTF-16LE
中文漢字:人
位元組數:2;編碼:GB2312
位元組數:2;編碼:GBK
位元組數:2;編碼:GB18030
位元組數:1;編碼:ISO-8859-1
位元組數:3;編碼:UTF-8
位元組數:4;編碼:UTF-16
位元組數:2;編碼:UTF-16BE
位元組數:2;編碼:UTF-16LE
我們可以看到,我們從網站擷取的内容簡介是UTF-8模式存取在字元串String中的,但是在java的String内部漢字當成一個位元組的字元(UCS2字元)處理了。是以周遊的時候截取漢字隻用一個字元就可以,但是卻在UTF-8中跳過了3個字元。舉例說明:
假如有個字元串String str="我是帥哥火";存儲的時候用的是UTF-8編碼。
是以String的.getBytes("UTF-8").length位元組數是15。但是str.charAt(0),将擷取“我“這個漢字,但是在UTF-8中你已經跳過了2個位元組,預設截取第二個字元同樣是str.charAt(1),得到“是”。
但是假如我的字元串是String str="我liyuanjing是shuaige";存儲的時候用的是UTF-8編碼。
是以String的.getBytes("UTF-8").length位元組數是23。但是我要截取多少字元算的是漢字和符号或字母,假如我要截取“我liyuanjing是”,那麼需要輸入的是16,但是截取隻用了12,少了4個位元組的內插補點,這就是下面截取字元串方法為什麼每截取一個漢字,總數要減去2的原因。
截取漢字的ApplyUtils.class代碼如下:
public static boolean isChineseChar(char c) throws UnsupportedEncodingException { // 如果位元組數大于1,是漢字
return String.valueOf(c).getBytes("UTF-8").length > 1;
}
public static String substring(String orignal, int count)
throws UnsupportedEncodingException {
// 原始字元不為null,也不是空字元串
if (orignal != null && !"".equals(orignal)) {
// 将原始字元串轉換為UTF-8編碼格式
orignal = new String(orignal.getBytes(), "UTF-8");//
// System.out.println(orignal);
//System.out.println(orignal.getBytes().length);
// 要截取的位元組數大于0,且小于原始字元串的位元組數
if (count > 0 && count < orignal.getBytes("UTF-8").length) {
StringBuffer buff = new StringBuffer();
char c;
for (int i = 0; i < count; i++) {
c = orignal.charAt(i);
buff.append(c);
if (ApplyUtils.isChineseChar(c)) {
// 遇到中文漢字,截取位元組總數減2
--count;
--count;
}
}
return new String(buff.toString().getBytes(),"UTF-8");
}
}
return orignal;
}
這段雖然是Copy别人的,但是那個網址的代碼有個錯誤,我這裡改正了,原理我也講清楚了。希望大家應該聽懂了。
②dp轉換成px。
動态代碼中,并不能設定成dp格式,隻有px格式,這個時候,就需要把你常用的dp轉換成px。
dp是虛拟像素,在不同的像素密度的裝置上會自動适配
在320x480分辨率,像素密度為160,1dp=1px
在480x800分辨率,像素密度為240,1dp=1.5px
計算公式:1dp*像素密度/160 = 實際像素數
那麼就得到如下代碼:
public static int dip2px(Context context, float dpValue) {
final float scale = context.getResources().getDisplayMetrics().density;
return (int) (dpValue * scale + 0.5f);
}
至于為什麼會是這樣,你把這段代碼拷貝到Android Studio中,點選getDisplayMetrics()檢視源代碼,你就清楚,就是上面的公式進行換算的。
6.異步加載
這裡簡單的用AsyncTask,到優化listView博文會具體改進的。
代碼在NewsFragment.class中:
private class ImportAsyncTask extends AsyncTask<Void,Integer,List<MessageItem>>{
@Override
protected List<MessageItem> doInBackground(Void... params) {
List<MessageItem> messageItemList=null;
try {
messageItemList=LYJJsoupWangYiTUtils.jsoupImportantNews("http://news.163.com/mobile/");
} catch (Exception e) {
e.printStackTrace();
}
return messageItemList;
}
@Override
protected void onPostExecute(List<MessageItem> items) {
if(items.size()==0){
}else{
importAdapter=new ImportantNewsAdapter(items,getActivity());
importListView.setAdapter(importAdapter);
}
}
}
在打開24小時要聞中調用它:
//24小時新聞顯示按鈕
this.importNews.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
importLayout.setAnimation(translateShow);//設定動畫
translateShow.start();//執行動畫
importLayout.setVisibility(View.VISIBLE);//顯示界面
new ImportAsyncTask().execute();//加載網絡新聞
}
});
本章節的源代碼如下:
http://download.csdn.net/detail/liyuanjinglyj/9223397
運作程式,得到如下結果: