這個例子是我去年鑽研Android應用時透過Google搜尋找到的,此文章源始於對岸大陸http://blog.csdn.net/caowenbin,自認學習Android也有些時日及一定功力,當時乍然一看到此範例之前段說明時感覺有點驚奇,因此對大陸有些從事IT程式開發人員改觀且刮目相看,在本島上個人週遭所認識很多的電腦人本位主義相當重,很少像大陸IT如此熱衷及分享研究心得,說真的,已逐漸感受到台灣技術已被對岸超出甚多,對未來想從事程式開發的人期能急起直追趕上進度,否則市場總有一天會被對岸的人所瓜分取代的(此為作者長期觀察大陸最近幾年技術快速猛進所提之肺腑之言,供各位深思之)
回到正題,此例當時花了幾天時間徹底研讀過,除對源碼中增加注釋外,其餘均不作更動(※尊重原始文章作者之著作權),只是將此篇文章經個人消化後去蕪存菁加以整理為日後作開發之參考,在此提到個人對此範例深究後之觀點:
²  此範例從始至末原作者對概念及程式代碼上均交待得非常清楚,但不適合初等學習者,換言之,須具備一定開發經驗者才能探究其真正意義所在(很多稍具Android基礎的開發者無法一下子解讀程式之某些特點,經個人詳加說明後才壑然開朗其整體設計思路)
²  強烈建議未來想從事Android程式開發人員學習此範例,此會對日後程式開發上(指的是對解決問題之邏輯分析及程式代碼之構建)絕對有莫大的助益,個人也將此範例作整理為一個教學專題提供給學員作觀摩,藉此給想成為一位專業且出色之程式設計員應訓練至此境界才能於市場立足。
下圖為使用對岸新浪微博手機用戶端Android版本察看新浪微博網頁所呈現之介面:
 從圖中之主體可觀察出其清單(ListView)方塊部份包含很複雜的部分,既要能顯示頭像、微博內容,又要能在網頁內容中顯示表情、圖片、@某人、URL元素混雜在一起。下圖為仿照此介面所開發出之運行結果畫面,其內所用的圖片均來自新浪微博。
茲將此程式之整體開發流程分述如下:
Ø   首先分析原網頁介面將其切割找出所包含之資料部份,主要的資料抽象可定義為Site(網站)Blog(博文)User(用戶),這些資料模型之相依性及其具體成員定義如下圖所示: 
※參考http://open.weibo.com/中的開發文檔(源碼來源於一個聚合了新浪微博和搜狐微博兩家網站的Android應用)
Ø   定義資料模型之代碼
資料模型找出之後,接著為這些資料模型建構其類別代碼使之成為可供後續開發使用之基本元件,各資料模型之類別代碼列述如下:
n   User.class
package com.wenbin.test.site;

public class User{
   private String profileImageUrl="http://tp3.sinaimg.cn/1500460450/50/1289923764/0";   //瀏覽特定網頁
   private String screenName="测试";
   private boolean verified=false;

   public User(){    //建構函式
         
   }

   public String getProfileImageUrl(){     //取得網頁URL
          return profileImageUrl;
   }

   public String getScreenName(){  //取得畫面名稱
          return screenName;
   }

   public void setProfileImageUrl(String profileImageUrl) {
//設定網頁URL
          this.profileImageUrl = profileImageUrl;    
   }

public void setScreenName(String screenName) {
//設定畫面名稱
          this.screenName = screenName;
   }

   public void setVerified(boolean verified) { //設定識別條件
          this.verified = verified;
   }

   public boolean isVerified(){          //判斷識別條件
          return verified;
   }
}

n   Blog.class
package com.wenbin.test.site;

import java.util.Date;

public class Blog implements Comparable<Blog>{

private Date createAt=new Date(System.currentTimeMillis());  //取得系統目前日期
   private Blog retweetedBlog;
   private String text="就算把我打的遍体鳞伤也见不得会[]http://blog.csdn.net/caowenbin @移动云_曹文斌 "; 
      //
測試用之字串資料
   private String smallPic="";
   private String source="IE9";
   private User user;
   private Site site;

   public Blog(){                 //建構函式1

   }
  
   public Blog(Site site){     //建構函式2
          this.site=site;
   }

   public boolean isHaveRetweetedBlog(){          //判斷識別條件
          return retweetedBlog!=null;
   }
  
   public Blog getRetweetedBlog(){  //取得RetweetedBlog資料
          return retweetedBlog;
   }


   public String getText(){  //取得測試之字串資料
          return text;
   }

   public User getUser(){    //取得用戶資訊
          return user;
   }
  
   public String getSmallPic(){   //取得SmallPic資料
          return smallPic;
   }

   public void setRetweetedBlog(Blog retweetedBlog) {
           //
設定RetweetedBlog資料
          this.retweetedBlog = retweetedBlog;
   }

   public void setText(String text) {   //設定測試之字串資料
          this.text = text;
   }
  
   public String getInReplyUserScreenName(){   //取得畫面名稱
          if (retweetedBlog!=null && retweetedBlog.getUser()!=null)
                return retweetedBlog.getUser().getScreenName();
          else
                return "";          
   }
  
   public String getInReplyBlogText(){   //取得Blog內容
          if (retweetedBlog!=null)
                return retweetedBlog.getText();
          else
                return "";    
   }
  
   public void setPic(String smallPic){     //設定SmallPic
          this.smallPic=smallPic;
   }

   public void setUser(User user) {  //設定用戶資訊
          this.user = user;
   }

   public int compareTo(Blog another) {  //判斷比較Blog建置之先後
          int ret=0;

          if (this.createAt.before(another.createAt)){
                ret=-1;
          }
          else if (this.createAt.after(another.createAt)){
                ret=1;
          }
          else{
                ret=0;  
          }

          return ret;
   }

   public void setSource(String source) {  //設定資源
          this.source = source;
   }

   public String getSource() {                //取得資源
          return source;
   }

   public void setSite(Site site) {                  //設定網站
          this.site = site;
   }

   public Site getSite() {                        //取得網站
          return site;
   }


n   Site.class
package com.wenbin.test.site;

import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;

public abstract class Site{

   protected Set<Blog> blogs=new TreeSet<Blog>();
   protected String name;
   protected Map<String,String> faceMap=new HashMap<String,String>();

   public Site() {           //建構函式
          onConstruct();   //呼叫自訂函式
   }

   protected abstract void onConstruct();            //由繼承之子類別實作之
  
   public Map<String, String> getFaceMap() {//取得Face圖形資訊集合
          return faceMap;
   }
  
   public Set<Blog> getBlogs(){            // 取得Blog物件集合
          return blogs;
   }
  
   public long getBlogsCount(){       //取得Blog物件之數目
          return blogs.size();
   }
  
   public void addBlog(Blog blog){   //加入Blog物件
          blogs.add(blog);
   }
  
   public void removeBlog(Blog blog){    //移除Blog物件
          blogs.remove(blog);
   }
  
   public Iterator<Blog> getBlogsIterator(){//取得Blog集合之Iterator
          return blogs.iterator();
   }
  
   public void clearBlogs(){       //清除Blog集合
          blogs.clear();
   }

   public String getName(){             //取得Site名稱
          return name;
   }
} 

n   SinaSite.class
package com.wenbin.test.site;

public class SinaSite extends Site {

      protected void onConstruct(){           //實作繼承父類別之抽象函式
          name="新浪微博";
          initFaceMap();          //自訂方法(初始化Face資訊)
   }

   private void initFaceMap(){          //建立Face 資訊
          faceMap.put("[呵呵]", "hehe");
          faceMap.put("[嘻嘻]", "xixi");
          faceMap.put("[哈哈]", "haha");
          faceMap.put("[爱你]", "aini");
          faceMap.put("[]", "yun");
          faceMap.put("[]", "lei");
   }
} 
上述所建之資料模型(類別)最主要可作為應用之基本元件來使用 
Ø   介面設計:綜觀整個介面,可分成上下兩部份,其中上層為操作條,下層則為主體的清單(ListView)方塊,分別將其特性列述如下:
上層之操作條可分解成三個部分,其中左側為微博按鈕,中間為用戶名稱顯示,右側則為刷新按鈕。其中兩個按鈕採用相同之設計風格,分別帶有常規按下兩種狀態,欲達此目的之作法可如下:
1.       drawable資料夾下建立兩個xml文件,分別對應了兩個按鈕;
2.       每個xml檔中使用selector標籤定義常規狀態和按下狀態之兩個圖片資源;
3.       Activity的主佈局中使用ImageButton控制項,於屬性定義中指定按鈕的background為透明,並指定src為之前定義的兩個xml
 下面是微博按鈕xml檔的內容:
<?xml version="1.0" encoding="utf-8"?>  
<selector xmlns:android="http://schemas.android.com/apk/res/android">  
<item android:state_pressed="true" android:drawable="@drawable/title_new_selected" />  
     <item android:drawable="@drawable/title_new_normal" />  
</selector>   
 下面是刷新按鈕xml檔的內容:
<?xml version="1.0" encoding="utf-8"?>  
<selector xmlns:android="http://schemas.android.com/apk/res/android">  
       <item android:state_pressed="true" android:drawable="@drawable/title_reload_selected" />  
       <item android:drawable="@drawable/title_reload_normal" />  
</selector>   
        接著在main.xml檔中進行必要元件安排之佈局規劃,對於這三個控制項元素而言,因其彼此間有明確的相對位置關係,故採用RelativeLayout合適,其xml檔之內容如下所示:
Main.xml
<RelativeLayout   
    android:layout_width="fill_parent" android:layout_height="44dp"  
    android:background="@drawable/titlebar_lightgray_bg" android:orientation="horizontal">  
    <ImageButton android:id="@+id/BtnWrite"  
        android:layout_width="wrap_content" android:layout_height="fill_parent"  
        android:layout_alignParentLeft="true" android:background="@android:color/transparent"  
        android:src="@drawable/write_button">  
    </ImageButton>  
    <TextView android:id="@+id/TextViewUsername"  
        android:layout_width="fill_parent" android:layout_height="fill_parent"  
        android:textColor="@color/black" android:gravity="center" android:textSize="18sp">  
    </TextView>  
    <ImageButton android:id="@+id/BtnRefresh"  
        android:layout_width="wrap_content" android:layout_height="fill_parent"  
        android:layout_alignParentRight="true" android:background="@android:color/transparent"  
        android:src="@drawable/refresh_button">  
    </ImageButton>  
</RelativeLayout>  
在此須指定RelativeLayoutbackground為背景圖片,使用到之圖片列述如下:
main.xml主佈局中以ImageButtonTextView控制項和RelativeLayout進行UI規劃之先期工作,接著要進行主體格局下層之規劃。回顧新浪微博的瀏覽介面可知其應用了ListView控制項,但仔細瞧瞧覺得在ListView中又難以實現這種圖文並茂且具有佈局要求形態之複雜顯示,另注意到此清單方塊的第一item最後一item是固定的,分別放置了“刷新”“更多”兩個item,無論清單方塊內有多少項,這兩個Item必存在於畫面上。故掌握思路為:保持這兩個Item作為清單方塊的一個不變的組成部分,而其它則用於填充資料部份 
欲實作上述所提而想出之解決方案為:基於上述觀察與分析,可通過為ListView添加HeaderViewFooterView來解決這兩個特殊的item問題,如此便可依實際需求進行這兩個View之佈局規劃,底下分別列述其 xml檔之規劃內容
n   bloglistheader.xml檔內容:
<?xml version="1.0" encoding="utf-8"?>  
<LinearLayout  
  xmlns:android="http://schemas.android.com/apk/res/android"  
  android:layout_width="fill_parent"  
  android:layout_height="fill_parent"  
  android:gravity="center"  
  android:background="@color/white">  
    <TextView android:id="@+id/textHeader"   
        android:layout_width="wrap_content"   
        android:layout_height="wrap_content"  
        android:paddingTop="15dp"  
        android:paddingBottom="15dp"  
        android:layout_gravity="center_horizontal"  
        android:text="@string/refresh"  
        android:textColor="@color/black"  
        android:textSize="15sp">  
    </TextView>  
</LinearLayout>  
n   bloglistfooter.xml內容:
<?xml version="1.0" encoding="utf-8"?>  
<LinearLayout  
  xmlns:android="http://schemas.android.com/apk/res/android"  
  android:layout_width="fill_parent"  
  android:layout_height="fill_parent"  
  android:gravity="center"  
  android:background="@color/white">  
    <TextView android:id="@+id/textfooter"   
        android:layout_width="wrap_content"   
        android:layout_height="wrap_content"  
        android:paddingTop="15dp"  
        android:paddingBottom="15dp"  
        android:layout_gravity="center_horizontal"  
        android:text="@string/more"  
        android:textColor="@color/black"  
        android:textSize="15sp">  
    </TextView>  
</LinearLayout>  
        有了此兩個佈局檔,就可以依實際需求彈性設計自訂ListView,故在此新建一個繼承自ListView BlogListView.class,並在該控制項完成佈局時加入上述之兩個佈局以達擴建格局之功能,此BlogListView類的代碼如下: 
package com.wenbin.test;

import com.wenbin.test.site.Site;
import android.content.Context;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.ListView;

public class BlogListView extends ListView {
private Site site;
public BlogListView(Context context) {            //建構函式1
       super(context);
}

public BlogListView(Context context, AttributeSet attrs) {
//建構函式2
       super(context, attrs);
}

public BlogListView(Context context, AttributeSet attrs, int defStyle) {                                                      //建構函式3
       super(context, attrs, defStyle);
}

@Override
protected void onFinishInflate() {                   //覆寫方法
       super.onFinishInflate();
       LayoutInflater li=LayoutInflater.from(getContext());
       View headerView=li.inflate(R.layout.bloglistheader, null);
       headerView.setOnClickListener(new OnClickListener(){
             @Override
              public void onClick(View v) {
                    refresh();           //更新方法
              }                
       });
       View footerView=li.inflate(R.layout.bloglistfooter, null);
       footerView.setOnClickListener(new OnClickListener(){
              @Override
              public void onClick(View v) {
                    more();             //再次更新方法
              }                
      });
       addHeaderView(headerView);
       addFooterView(footerView);
}

public void init(Site site){            //初始化Site之自訂方法
       this.site=site;
       if (site!=null && site.getBlogsCount()>0){
              setAdapter(new BlogAdapter(site.getBlogs(),getContext()));
       }
}
public void refresh() {
       //TODO:
}

public void more(){
       //TODO:
}

public Site getSite(){             //取得Site 物件
       return site;
}

}
由於是自訂控制項,所以要main.xml中加入它的話,得把佈局修改成如下:
<?xml version="1.0" encoding="utf-8"?>  
<LinearLayout   
    android:layout_width="fill_parent" android:layout_height="fill_parent"  
    xmlns:android="http://schemas.android.com/apk/res/android"  
    android:orientation="vertical">  
    <RelativeLayout   
        android:layout_width="fill_parent" android:layout_height="44dp"  
        android:background="@drawable/titlebar_lightgray_bg" android:orientation="horizontal">  
        <ImageButton android:id="@+id/BtnWrite"  
            android:layout_width="wrap_content" android:layout_height="fill_parent"  
            android:layout_alignParentLeft="true" android:background="@android:color/transparent"  
            android:src="@drawable/write_button">  
        </ImageButton>  
        <TextView android:id="@+id/TextViewUsername"  
            android:layout_width="fill_parent" android:layout_height="fill_parent"  
            android:textColor="@color/black" android:gravity="center" android:textSize="18sp">  
        </TextView>  
        <ImageButton android:id="@+id/BtnRefresh"  
            android:layout_width="wrap_content" android:layout_height="fill_parent"  
            android:layout_alignParentRight="true" android:background="@android:color/transparent"  
            android:src="@drawable/refresh_button">  
        </ImageButton>  
    </RelativeLayout>  
    <com.wenbin.test.BlogListView android:id="@+id/sinaList" android:layout_width="fill_parent"   //自訂控制項之標籤
        android:layout_height="fill_parent" android:clickable="true"  
        android:divider="@drawable/divider" android:dividerHeight="1dp">  
    </com.wenbin.test.BlogListView>  
</LinearLayout>  
其中之divider屬性是定義Item間的分隔條的。此時若模擬運行會發現其結果並沒有顯示出清單方塊,此是因為沒有資料可顯示所造成,等之後添加正式資料便會顯示結果資訊。在此提一個在開發中很重要之概念:Google提供之標準控制項無法達到應用之設計需求時,可直接繼承該類別後再分別覆寫其內之必要方法以彈性擴增不足之功能,此種作法是開發應用工程師必須瞭解之技術,此是很受用之竅門,要學會它。

<<下回再續>>

0 意見: