感冒了几天,今天总算正常了点,想了想我还有未完成的最后一类博文,马上就跳起来,接着奋斗了。废话就不多说了先上图看看网易新闻APP的标题栏24小时要闻如下:
我们即将完成的效果图:
图片显示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>
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); } } }
英文字母: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