虎扑体育客户端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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值