虎撲體育客戶端zen源碼學習筆記

SOURCE

==================

ZenLogin登錄

ZenLoginActivity

動態註冊Boardcast

    protected void onResume() {
        super.onResume();
        try {
            IntentFilter filter = new IntentFilter();
            filter.addAction(ZenLoginModel.ZEN_LOGIN_FINISHED);
            filter.addAction(ZenLoginModel.ZEN_LOGIN_FAILED);
            registerReceiver(mBoradcastReceiver, filter);
        } catch (Exception e) {
            e.printStackTrace();
        }

    }

broadcastReceiver

    private BroadcastReceiver mBoradcastReceiver = new BroadcastReceiver() {

        @Override
        public void onReceive(Context context, Intent intent) {
            mLoading.hide();
            String action = (String) intent.getAction();
            if (action.equals(ZenLoginModel.ZEN_LOGIN_FINISHED)) {
                ZenMyBoardsModel boardModel = ZenMyBoardsModel.getInstance();
                boardModel.load();                                                        //加載板塊
                ZenNotificationModel.getInstance().load();
                AppMsg appMsg = AppMsg.makeText(ZenLoginActivity.this, "登錄成功",
                        AppMsg.STYLE_INFO);
                appMsg.show();
                Handler handler = new Handler(getMainLooper());
                handler.postDelayed(new Runnable() {

                    @Override
                    public void run() {
                        finish();
                    }
                }, 2000);
                //...
         }
      }

注意,需要在onpause/onDestory中unregister:

unregisterReceiver(mBoradcastReceiver);

在ZenLoginModel處理完登錄動作後

mContext.sendBroadcast(new Intent(ZEN_LOGIN_FINISHED));

broadcastReceiver 異步退出activity. broadcastReceiver / activity 不在同一個線程嗎.

登錄最終目的就是得到登錄賬號的cookie.代碼中保存爲ZenUtils.gettoken().如果已經登錄過,會保存在SharedPreferences中.否則從web獲取,代碼:

        public void OnResponse(String response) {
            try {
                if (response != null) {
                    System.out.println("response: " + response);
                    JSONObject json = new JSONObject(response);
                    JSONObject result = json.getJSONObject("result");
                    JSONObject user = result.getJSONObject("user");
                    userInfo.userName = user.getString("username");
                    userInfo.uid = user.getString("uid");
                    userInfo.token = user.getString("token");
                    isLogedin = true;
                    save();
                    fetchMSGToken();
                    mContext.sendBroadcast(new Intent(ZEN_LOGIN_FINISHED));
                }
                return;
            } catch (Exception e) {
                e.printStackTrace();
            }
            System.out.println("zen login failed!");
            mContext.sendBroadcast(new Intent(ZEN_LOGIN_FAILED));
        }

=============

以ZenMyBoardsModel爲例分析model代碼

此model使用get方法獲得板塊數據.
示例url

example: http://mobileapi.hupu.com/1/1.1.1/bbs/getusercollectedboards?token=15538936|aWxvdHVv|1f4b|198a84db2223ce42d77a62d67a64517a|2223ce42d77a62d6

第一次接觸web客戶端,從域名可以看出原來網站爲了拓展都預留一個api用於查詢數據庫?

連接,encoding(to utf-8),發送,getResponse,decoding(from utf-8),得到:

{“islogin”:1,”result”:[{“fid”:”151”,”cid”:”198”,”name”:”國家隊-世界盃”},{“fid”:”63”,”cid”:”234”,”name”:”同城約戰”},{“fid”:”34”,”cid”:”174”,”name”:”步行街”},{“fid”:”130”,”cid”:”1”,”name”:”籃球場”},{“fid”:”1048”,”cid”:”1”,”name”:”溼乎乎的話題”},{“fid”:”24”,”cid”:”232”,”name”:”中國籃球”},{“fid”:”2552”,”cid”:”1”,”name”:”深籃討論區”},{“fid”:”43”,”cid”:”1”,”name”:”籃球圖片”},{“fid”:”37”,”cid”:”1”,”name”:”NBA選秀-NCAA”},{“fid”:”1028”,”cid”:”1”,”name”:”NBA2K專區”},{“fid”:”3864”,”cid”:”57”,”name”:”極限運動X-GAMES”},{“fid”:”82”,”cid”:”1”,”name”:”凱爾特人區”},{“fid”:”3312”,”cid”:”82”,”name”:”凱爾特人生活區”},{“fid”:”85”,”cid”:”1”,”name”:”騎士專區”},{“fid”:”105”,”cid”:”1”,”name”:”馬刺專區”},{“fid”:”3316”,”cid”:”105”,”name”:”馬刺生活區”},{“fid”:”127”,”cid”:”1”,”name”:”快船專區”}]}

另外返回數據json使用utf-8編碼.直接用在線解碼會不成功.
如:
u80a5\u80a0\u714e\u86cb 肥腸煎蛋
應該使用 肥腸煎蛋 refer to link

Fiddler header :

User-Agent: Fiddler
Content-Type: application/json; charset=utf-8
Host: mobileapi.hupu.com

設計思路:
private ZenOnResponseListener mOnResponseListener :

ZenJSONUtil.WriteJSONToFile(ZenJSONUtil.ZEN_MY_BOARDS_JSON, jsonString);
ZenJSONUtil.reloadMyBoards();

版塊主題列表列表加載

ZenMenuFragment.java

mBoards.setOnChildClickListener(new OnChildClickListener() {
            public boolean onChildClick(ExpandableListView parent, View v,
                    int groupPosition, int childPosition, long id) {
                //切換到板塊主題列表
                switchContent(board.get("fid"), board.get("name"));
                            ZenThreadsFragment fragment = new ZenThreadsFragment();
                            //...
                            ac.switchContent(fragment, name);
}
}

以下的調用不一定在同一個函數中,只按照timeline排序. 縮進表示層次關係(可能跨多層)
ZenThreasFragment.java ZenThreadsModel.java

        actureListView.setOnItemClickListener(...);
        //獲取主題列表數據 model init
        mModel = new ZenThreadsModel(mContext, fid);
        mModel.refresh();
                load(mPage);//ZenThreadsModel.java
                    String url = String.format(ZenThreadsModel.ZenThreadsURL, mFid, page);

                public void OnResponse(String response)         
                Intent intent = new Intent(ZenThreadsModel.DidFailedLoad);
            mContext.sendBroadcast(intent);
         public void onReceive(Context context, Intent intent) //BroadcastReceiver mBroadcastReceiver
                       mThreadsAdapter.array = mModel.threads;
                //Adapter更新listView
                mThreadsAdapter.notifyDataSetChanged();
                mList.scrollTo(0, 0);

關於Adapter和listView的初始化見

    ZenThreadsFragment.onActivityCreated

帖子內容加載

重點

    actureListView.setOnItemClickListener(new OnItemClickListener()
    Intent intent = new Intent(this, ZenContentActivity.class);

首先切換到另一個Activity了,具體網絡通信模型和前面類似.

ZenReplyModel.java

下面使用這個 thread demo url 做例子:

http://mobileapi.hupu.com/1/1.1.1/bbs/getthreaddata?type=2&tid=12063126&boardpw=&token=15538936|aWxvdHVv|1f4b|198a84db2223ce42d77a62d67a64517a|2223ce42d77a62d6

return data:

{“islogin”:1,”result”:{“tid”:”12063126”,”fid”:”34”,”username”:”肥腸煎蛋”,”uid”:”20676879”,”subject”:”在火車上收一張圖,發現自己的世界灰暗了”,”postdate”:”1425009205”,”lastpost”:”1425064033”,”lastposter”:”李時念”,”replies”:”693”,”locked”:”0”,”digest”:”0”,”lights”:”8”,”recs”:”15”,”content”:”
衆jr看這張圖什麼顏色?我看的是淡紫加有點奇怪的綠色,但同學和下面的英文都告訴我,他們看得是白金或者藍黑。
<%/center>
來自 Zen For Android <%/a>”,”boardname”:”步行街”,”boardurl”:”“,”url”:”“}}

此時webView只有帖子主題沒回復,處理數據函數

private void parserThreadDataResponse(String response)完成了這些工作:

  1. 返回json保存在threadData對象
  2. 發送廣播Intent successIntent = new Intent(ZenReplyModel.ZenThreadDataDidFinishedLoad);

廣播後最終在private void onThreadDataFinished(ZenThreadData data)處理主題數據後續.加載完主題數據,添加到webview後,開始加載回覆數據,主要內容:

thread.loadUrl("javascript:clearPost('')");        //清除頁面內容
String .format("javascript:addMainPost('%s', '%s', '%s', '%s', '%s', '%s');",
                                data.subject, postInfo, data.postdate,
                                data.author, "樓主", data.content);        
//load it ..
mModel.loadReplies(mPage);                        //加載回覆數據

加載了兩條javascrpit語句.這些語句都在 hupu_post.html 模型中.

loadRelies 重複了上面類似的過程.加載json數據,調用javaScript語句,添加網頁內容.完成後發送廣播:

Intent successIntent = new Intent(ZenReplyModel.ZenRepliesDidFinishedLoad);

類似的,接收廣播後處理下一步渲染:

private void onThreadRepliesFinished()

這一步開始收尾處理view:

pullToRefreshWebView.onRefreshComplete();  //webview complete loading.
mLoading.hide();

and finished reply post handling over javascript.
e.g.

thread.loadUrl("javascript:addLightTitle('熱門跟帖', 'true')");
//...
ZenThreadReply reply = model.lightReplies.get(i);
String js = String
.format("javascript:addLightPost('%s', '%s', '%s', '%s', '%d', '%s')",
reply.author, "", reply.light, reply.content,
i, reply.pid);
thread.loadUrl(js);

總結

客戶端是通過
thread.loadDataWithBaseURL(“file:///android_asset/”, html, “text/html”,
“utf-8”, null);
加載模板.再獲得API json數據,結合javascript腳本生成帖子的.可以比較m.hupu.com 和zen生成的頁面並不一樣.如圖:

藉助端點工具查看各階段的webView.
至於這個模板(assert/hupu_post.html)從何而來,我就不得而知了.

Fragment的應用

android Fragments詳解四:管理fragment

下面仍然是ZenContentActivity相關的代碼.

ZenMenuBar的實現(8個按鍵動作)

Menu 封裝了所有使用這個MenuBar的按鈕操作,使用了Adapter設計模式的,其中按鈕相應函數:

public void OnMenuItemClick(int type) {
         switch (type) {
         case ZenMenuBar.MENU_LIGHT:
             light();
             break;
         case ZenMenuBar.MENU_REPLY:
             reply();
             break;
         case ZenMenuBar.MENU_COPY:
             copy();
             break;
         case ZenMenuBar.MENU_PM:
             pm();
             break;
         case ZenMenuBar.MENU_REFRESH:
             refresh();
             break;
         case ZenMenuBar.MENU_COMMENT:
             comment();
             break;
         case ZenMenuBar.MENU_RECOMMEND:
             recommend();
             break;
         case ZenMenuBar.MENU_ARCHIVE:
             archive();
             break;
         }
}

light,refresh操作和前面類似,加載這個文件:

String url = String.format(Locale.getDefault(), ZEN_LIGHT_URL,
mFid, mTid, pid, URLEncoder.encode(token, "utf-8"));

然後處理完response json後在 mBroadcastReceiver 調用javascript渲染:

                mLoading.hide();
                String lights = intent.getStringExtra("light");
                String js = String.format(Locale.getDefault(),
                        "javascript:lightSuccess('%s', '%s', %d)", lights,
                        mReplyData.pid, mArea);
                thread.loadUrl(js);
                AppMsg appMsg = AppMsg.makeText(ZenContentActivity.this,
                        "點亮成功", AppMsg.STYLE_INFO);
                appMsg.show();

recommend實現

推薦使用和web端一樣的鏈接,但是他們的cookie是通用的:

private static final String ZEN_RECOMMEND_URL = "http://bbs.hupu.com/indexinfo/buddys.php";

            String tokenEncoded = URLEncoder.encode(token, "utf-8");
            mConnection = new ZenURLConnection(ZEN_RECOMMEND_URL);
            mConnection.setOnResponseListener(mRecommendListener);
            mConnection.setHttpMethod("POST");
            mConnection.addRequestHeader("Content-Type",
                    "application/x-www-form-urlencoded ; charset=UTF-8");
            mConnection.addRequestHeader("Cookie", "u=" + tokenEncoded + ";");
            mConnection.addRequestHeader("X-Requested-With", "XMLHttpRequest");
            mConnection.addRequestHeader("Referer", "http://bbs.hupu.com/"
                    + mTid + ".html");

            String httpBody = "fid=" + mFid + "&act=rc" + "&cid=" + mTid
                    + "&title=" + URLEncoder.encode(title, "utf-8") + "&rmmsg="
                    + URLEncoder.encode(content, "utf-8") + "&type=1";
            mConnection.setHttpBody(httpBody);
            mConnection.startAsychronous();

Comment/Reply實現

Comment實現比上面要複雜些.
上傳圖片:

String token = ZenUtils.getToken();
if (token != null) {
    String boundary = "----pluploadboundary" + ZenUtils.timestamp();
    mUploadConnection = new ZenURLConnection(ZEN_UPLOAD_URL);
    mUploadConnection.setHttpMethod("POST");
    mUploadConnection.addRequestHeader("Cookie",
            "u=" + URLEncoder.encode(token, "utf-8"));
    mUploadConnection.addRequestHeader("Content-Type",
            "multipart/form-data; boundary=" + boundary);
    InputStream body = boundary(image, boundary, isGif);
    if (body != null) {
        mUploadConnection.setHttpInputStream(body);
        String response = mUploadConnection.startSychronous();
        if (response != null) {
            System.out.println("upload: " + response);
            JSONObject json = new JSONObject(response);
            if (json.has("pic")) {
                String url = json.getString("pic");
                return url;
            }
        }
    }
}

注意boundary 是內容分割符.客戶端往服務器上傳內容需要靠分割符識別不同類型多媒體的request.參考rfc2616 session 19.2.請求主體在boundary中構造.請求成功後返回這樣的一個key {pic:url} 的json數據reponse.
然後來看完整的comment過程是如何調用上面的上傳函數:

ZenAssetsModel model = ZenAssetsModel.getInstance();
ArrayList<Map<String, Object>> images = model.getAssets();
ArrayList<String> urls = new ArrayList<String>();
ZenPostModel postModel = new ZenPostModel(mFid);
for (Map<String, Object> image : images) {
    String type = (String) image.get("type");
    InputStream obj = (InputStream) image.get("input");
    String url = null;
    if (type.equals("jpg")) {
        url = postModel.uploadImage(obj, false);
    } else {
        url = postModel.uploadImage(obj, true);
    }

    if (url != null) {
        urls.add(url);
    }
}

最後上傳評論:

if (pid != null && !pid.equals("")) {
String response = oldcomment(content, pid, urls);
return response;
} else {
String response = newcomment(content, pid, title, urls);

newcomment 提交的header在前面都有用到過.cookie,refer,User-Agent.body部分含有複雜的信息構建了一段html段落.包括引用,作者,回覆樓層,和”來自Zen”尾巴等等.部分代碼:

contentBuf.append(content);
if (urls != null) {
    for (String url : urls) {
        contentBuf.append("<br><br><img src=\"" + url
                + "\"><br><br>");
    }
}
contentBuf.append(ZEN_TAIL);
//ZENTAIL 爲content添加尾巴          
private static final String ZEN_TAIL = "<br><small class=\"f666\"><a style=\"color:#666\" href=\"http://rogerqian.github.com/zen_1.2.1.apk\" target=\"_blank\">來自 Zen For Android</a></small>";

END

就寫到這了.時間軸上這是最後停筆的地方.

我只是想接觸下http協議.老早之前就大概明白一個互聯網產品客戶端是怎麼寫出來的了.後面的坑也不填了~反正也沒人看.寒假本來只是想看兩個客戶端源碼,接觸瞭解下http,web服務器相關知識.作爲非計算機專業科班出身的學生的一個知識補充.還買了http翻了一半.忽然驚醒還有幾個月就要找實習了,才覺得補充過頭了,竟然耗了整整一個寒假的時間.趕緊打住回到底層代碼~爲那個實習工作努力去!

關於javaScript調用ZenBridge函數

refer to :
Android addJavaScriptInterface

第三方插件

SherlockActivity

只需要添加以下代碼

    public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()) {
        case android.R.id.home:
            toggle();
            break;
        case R.id.zen_new_post:

即可實現如下效果:
zen.id.new_post is one item of a menu resource.

PullToRefreshListView

Zen客戶端中 MVC模型的應用:

設計思路 | 相關知識


碎碎念

基礎回顧–抽象類概念

public abstract class BaseExpandableListAdapter implements ExpandableListAdapter,
        HeterogeneousExpandableList

implements 無需實現

ArrayList 也能這樣遍歷

public ArrayList<ZenTopicData> topics;
for (ZenTopicData topic : topics)

//ArrayList 也能這樣遍歷

移除一個inflate 動態加載的View

    /**
     * 移除一個inflate 動態加載的View
     * @param view
     */
    private void removeFromSuperView(View view) {
        ViewGroup superView = (ViewGroup) view.getParent();
        if (superView != null) {
            superView.removeView(view);
        }
    }

Instantiates a layout XML file into its corresponding

/*
 * Instantiates a layout XML file into its corresponding {@link android.view.View}
* objects. It is never used directly. Instead, use
* {@link android.app.Activity#getLayoutInflater()} or
* {@link Context#getSystemService} to retrieve a standard LayoutInflater instance
* that is already hooked up to the current context and correctly configured
 * for the device you are running on.  For example:
*/
LayoutInflater inflater = (LayoutInflater)context.getSystemService
       (Context.LAYOUT_INFLATER_SERVICE);

動態生成View多使用FrameLayout

   inflate.inflate(R.layout.zen_menu_bar_more, null);

xml:

    <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:id="@+id/zen_menu_container"
       android:layout_width="match_parent"
       android:layout_height="match_parent" >

另一種oclick監聽方法

            <ImageButton
                android:contentDescription="@null"
                android:id="@+id/zen_back_btn"
                android:onClick="OnBottomItemClick" />

java:

    public void OnBottomItemClick(View v) {
        switch (v.getId()) {
        case R.id.zen_back_btn:
            finish();
            break;
        case R.id.zen_prev_btn:
            hint();
            mLoading.show("正在加載...");
            mModel.prev();
            break;
// case etc..
        }

快速寫入應用私有文件

OutputStream output = context.openFileOutput(fileName, Context.MODE_PRIVATE);

other

這個開源版本中,有一些廣告插件的代碼.但是release中看不到廣告.所以不做研究
導入工程

  1. 需要導入所有 proprerity-android-library-工程源碼並打開

佈局原理
挑選一些佈局方案分析一下.首先是主題列表中獨立的自定義View
效果圖是這樣的:

回覆數量那裏會有一個僞隨機的顏色變化,實現多彩效果.
其實是這樣弄出來的:

        <FrameLayout
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignParentRight="true"
            android:layout_marginRight="10dp"
            android:layout_marginTop="5dp" >

            <ImageView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:contentDescription="@null"
                android:src="@drawable/comment_box" />

            <TextView
                android:id="@+id/zen_thread_replies"
                android:layout_width="24dp"
                android:layout_height="16dp"
                android:layout_gravity="top"
                android:background="@drawable/mid_green"
                android:gravity="center"
                android:textColor="#fff"
                android:textSize="11sp" />
        </FrameLayout>

ZenThreadsAdapter.java:

public View getView(int position, View convertView, ViewGroup parent) {
    replies.setBackgroundResource(color);
//...
}
    private int colorForReplies(int replyNum) {
        int index = replyNum % colors.length;
        return colors[index];
    }

    private int colors [] = {
            R.drawable.orange,
            R.drawable.navy,
            R.drawable.real_blue,
            R.drawable.purple,
            R.drawable.green
    };

原來是覆蓋一個大小合適的textview在圖片上面,用textView底色變色

pulltorefresh控件的源碼分析及使用

[TODO]

編碼問題
win7下默認unicode編碼.返回的數據如下(example)\u672a\u77e5\u9519\u8bef解決:使用fidder設置header

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章