OSCHINA客戶端完全剖析(三)分頁加載和詳情

分頁加載

Meizitu的實(shí)現(xiàn)

這個(gè)功能的實(shí)現(xiàn)跟MobileAPI的返回大有關(guān)聯(lián),我曾經(jīng)在學(xué)習(xí)Meizitu的時(shí)候看過其實(shí)現(xiàn),然后練手重構(gòu)了一下Meizitu。

它的MobileAPI是這樣的:

http://www.ourhfuu.com/meizitu.php?max_id=

http://www.ourhfuu.com/meizitu.php?since_id=

max_id是個(gè)輔助值,標(biāo)明你現(xiàn)在所有圖片中id最大的那個(gè)。分頁加載需要用到since_id,當(dāng)然,max_id用來對since_id進(jìn)行協(xié)助計(jì)算。

先看個(gè)實(shí)例http://www.ourhfuu.com/meizitu.php?since_id=999

易知,其API返回結(jié)果為從998開始到979結(jié)束,總共返回20條結(jié)果。
不信你可以換成http://www.ourhfuu.com/meizitu.php?since_id=979 再試一下。

客戶端那邊,Meizitu采用數(shù)據(jù)庫存儲+Loader,數(shù)據(jù)流上為

  1. 從網(wǎng)絡(luò)加載數(shù)據(jù)
  2. 加載好的數(shù)據(jù)保存至數(shù)據(jù)庫中
  3. Loader觸發(fā)觀察者效果,根據(jù)數(shù)據(jù)庫swapCursor()更新

因?yàn)镸obileAPI符合之前看到的特性,所以分頁加載數(shù)據(jù)再保存至數(shù)據(jù)庫時(shí)無需考慮存在數(shù)據(jù)重復(fù)的問題,又因?yàn)槭褂昧薒oader,所以不用進(jìn)行手動的更新UI顯示。

總的來說,這樣做法非常簡潔明了,但問題就在于數(shù)據(jù)流還繞了一圈,沒有從網(wǎng)絡(luò)加載的數(shù)據(jù)返回后就直接更新到UI來得自然。不過實(shí)際上也沒有什么問題就是了,反正已經(jīng)使用數(shù)據(jù)庫了,存數(shù)據(jù)庫這一步操作不能省略,而更新到UI的話是需要格式化從網(wǎng)絡(luò)返回的JSON數(shù)據(jù)的,直接讀數(shù)據(jù)庫讀的就是格式化后的數(shù)據(jù)。

OSCHINA的實(shí)現(xiàn)

OSCHINA的API返回不是JSON,而是XML,但這不是關(guān)鍵,關(guān)鍵在于其接口返回的數(shù)據(jù)有什么特性。

這里MobileAPI的參數(shù)比Meizitu多代碼更繞用法上也更復(fù)雜,最直觀的做法可以直接抓包然后改改參數(shù)看看效果。

不過,我這邊是直接看了看代碼分析的,先看綜合頁中的使用的API接口:

/**
     * 獲取新聞列表
     *
     * @param catalog
     *            類別 (1,2,3)
     * @param page
     *            第幾頁
     * @param handler
     */
    public static void getNewsList(int catalog, int page,
            AsyncHttpResponseHandler handler) {
        RequestParams params = new RequestParams();
        params.put("catalog", catalog);
        params.put("pageIndex", page);
        params.put("pageSize", AppContext.PAGE_SIZE);
        if (catalog == NewsList.CATALOG_WEEK) {
            params.put("show", "week");
        } else if (catalog == NewsList.CATALOG_MONTH) {
            params.put("show", "month");
        }
        ApiHttpClient.get("action/api/news_list", params, handler);
    }

直接就有一個(gè)第幾頁的參數(shù)page了,不過倒并不知道是不是跟Meizitu的API具有相同的特性。這也不算問題,后面直接看它怎么操作返回?cái)?shù)據(jù)就能推斷出來了。

經(jīng)過一番閱讀,發(fā)現(xiàn)網(wǎng)絡(luò)數(shù)據(jù)返回后會先進(jìn)行使用XStream的Bean解析,然后會變成一個(gè)類型為Bean的List傳到BaseListFragment#executeOnLoadDataSuccess(List<T> data)方法中:

protected void executeOnLoadDataSuccess(List<T> data) {
        if (data == null) {
            data = new ArrayList<T>();
        }

        if (mResult != null && !mResult.OK()) {
            AppContext.showToast(mResult.getErrorMessage());
            // 注銷登陸,密碼已經(jīng)修改,cookie,失效了
            AppContext.getInstance().Logout();
        }

        mErrorLayout.setErrorType(EmptyLayout.HIDE_LAYOUT);
        if (mCurrentPage == 0) {
            mAdapter.clear();
        }

        for (int i = 0; i < data.size(); i++) {
            if (compareTo(mAdapter.getData(), data.get(i))) {
                data.remove(i);
                i--;
            }
        }
        int adapterState = ListBaseAdapter.STATE_EMPTY_ITEM;
        if ((mAdapter.getCount() + data.size()) == 0) {
            adapterState = ListBaseAdapter.STATE_EMPTY_ITEM;
        } else if (data.size() == 0
                || (data.size() < getPageSize() && mCurrentPage == 0)) {
            adapterState = ListBaseAdapter.STATE_NO_MORE;
            mAdapter.notifyDataSetChanged();
        } else {
            adapterState = ListBaseAdapter.STATE_LOAD_MORE;
        }
        mAdapter.setState(adapterState);
        mAdapter.addData(data);
        // 判斷等于是因?yàn)樽詈笥幸豁?xiàng)是listview的狀態(tài)
        if (mAdapter.getCount() == 1) {

            if (needShowEmptyNoData()) {
                mErrorLayout.setErrorType(EmptyLayout.NODATA);
            } else {
                mAdapter.setState(ListBaseAdapter.STATE_EMPTY_ITEM);
                mAdapter.notifyDataSetChanged();
            }
        }
    }

步驟非常簡單明確

  1. mCurrentPage==0現(xiàn)在是不是分頁的話,數(shù)據(jù)全部加載上去
        if (mCurrentPage == 0) {
            mAdapter.clear();
        }
  1. for循環(huán)去除現(xiàn)有的數(shù)據(jù)項(xiàng),也就是說,要么MobileAPI不具有Meizitu的返回特性,要么就是在某種情況下可能會有重復(fù)數(shù)據(jù)項(xiàng)
for (int i = 0; i < data.size(); i++) {
            if (compareTo(mAdapter.getData(), data.get(i))) {
                data.remove(i);
                i--;
            }
        }

compareTo的實(shí)現(xiàn)也是十分暴力,如果id存在規(guī)律的話,倒是可以采用二分之類的改進(jìn)一下:

protected boolean compareTo(List<? extends Entity> data, Entity enity) {
        int s = data.size();
        if (enity != null) {
            for (int i = 0; i < s; i++) {
                if (enity.getId() == data.get(i).getId()) {
                    return true;
                }
            }
        }
        return false;
    }
  1. 置adapter狀態(tài),然后添加新的數(shù)據(jù)項(xiàng):
        int adapterState = ListBaseAdapter.STATE_EMPTY_ITEM;
        if ((mAdapter.getCount() + data.size()) == 0) {
            adapterState = ListBaseAdapter.STATE_EMPTY_ITEM;
        } else if (data.size() == 0
                || (data.size() < getPageSize() && mCurrentPage == 0)) {
            adapterState = ListBaseAdapter.STATE_NO_MORE;
            mAdapter.notifyDataSetChanged();
        } else {
            adapterState = ListBaseAdapter.STATE_LOAD_MORE;
        }
        mAdapter.setState(adapterState);
        mAdapter.addData(data);

看addData的實(shí)現(xiàn),直接在數(shù)據(jù)集的末尾追加,然后通知改變即可:

public void addData(List<T> data) {
        if (mDatas != null && data != null && !data.isEmpty()) {
            mDatas.addAll(data);
        }
        notifyDataSetChanged();
    }

另外

其他的就是一些細(xì)節(jié)需要進(jìn)行注意,比如滑動狀態(tài)的細(xì)分處理。

對了,ListView最下面總會有個(gè)footerView進(jìn)來表示“正在加載...”之類,這個(gè)footerView的存在導(dǎo)致ListView永不為空,且計(jì)算數(shù)值時(shí)也要考慮它的因素,還有就是各種情況下的顯隱性了。


詳情

所謂的詳情界面,就是從資訊或者博客點(diǎn)擊item進(jìn)入后的Activity:

點(diǎn)擊過后的item項(xiàng)灰顯了,因?yàn)樗驯患尤肓说谝黄岬竭^的已讀列表之中。

跳轉(zhuǎn)的代碼看NewsFragment#onItemClick:

@Override
    public void onItemClick(AdapterView<?> parent, View view, int position,
            long id) {
        News news = mAdapter.getItem(position);
        if (news != null) {
            UIHelper.showNewsRedirect(view.getContext(), news);

            // 放入已讀列表
            saveToReadedList(view, NewsList.PREF_READED_NEWS_LIST, news.getId()
                    + "");
        }
    }

UIHelper也看過幾次了,可以得出結(jié)論,該應(yīng)用在設(shè)計(jì)上使用這個(gè)類作為中間層來統(tǒng)一管理UI切換的操作。
其中針對不同的url情況做了較多的判定封裝,就不進(jìn)行直接研讀了。因?yàn)樽畛R姷募礊樯厦娴慕貓D“資訊詳情”與“博客詳情”,所以我們直接查找字串然后定位具體實(shí)現(xiàn)的Activity即可。

最后發(fā)現(xiàn)實(shí)現(xiàn)類為這個(gè):

/**
 * 詳情activity(包括:資訊、博客、軟件、問答、動彈)
 *
 * @author FireAnt(http://my.oschina.net/LittleDY)
 * @created 2014年10月11日 上午11:18:41
 */
public class DetailActivity extends BaseActivity implements OnSendClickListener {

BaseActivity extends ActionBarActivity

這個(gè)類主要封裝了黑白主題的設(shè)置、ActionBar操作、Toast操作(注意使用的是自定義的一個(gè)CommonToast)、ProgressDialog等。

DetailActivity extends BaseActivity

非常容易理解,放了多個(gè)標(biāo)志位來區(qū)分進(jìn)行不同的Fragment操作:

public static final int DISPLAY_NEWS = 0;
    public static final int DISPLAY_BLOG = 1;
    public static final int DISPLAY_SOFTWARE = 2;
    public static final int DISPLAY_POST = 3;
    public static final int DISPLAY_TWEET = 4;
    public static final int DISPLAY_EVENT = 5;
    public static final int DISPLAY_TEAM_ISSUE_DETAIL = 6;
    public static final int DISPLAY_TEAM_DISCUSS_DETAIL = 7;
    public static final int DISPLAY_TEAM_TWEET_DETAIL = 8;
    public static final int DISPLAY_TEAM_DIARY = 9;
    public static final int DISPLAY_COMMENT = 10;

找到資訊詳情為:

@Override
    protected void init(Bundle savedInstanceState) {
        super.init(savedInstanceState);
        int displayType = getIntent().getIntExtra(BUNDLE_KEY_DISPLAY_TYPE,
                DISPLAY_NEWS);
        BaseFragment fragment = null;
        int actionBarTitle = 0;
        switch (displayType) {
            case DISPLAY_NEWS:
                actionBarTitle = R.string.actionbar_title_news;
                fragment = new NewsDetailFragment();
                break;

自頂向下分析吧,因?yàn)锽aseFragment之前的篇章已分析過,所以從這里開始:

CommonDetailFragment<T extends Serializable> extends BaseFragment

看持有的域:

    protected int mId;

    protected EmptyLayout mEmptyLayout;

    protected int mCommentCount = 0;

    protected WebView mWebView;

    protected T mDetail;

    private AsyncTask<String, Void, T> mCacheTask;

意料之中,使用WebView來實(shí)現(xiàn)網(wǎng)頁瀏覽,具有效果表現(xiàn)需要看服務(wù)器端的適配情況了。

看布局文件,發(fā)現(xiàn)其標(biāo)題、作者、時(shí)間三項(xiàng)是使用原生控件來做的:

        <ScrollView
            android:id="@+id/sv_news_container"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:fadingEdge="none"
            android:scrollbars="vertical">

            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:orientation="vertical" >

                <LinearLayout
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:background="@color/white"
                    android:orientation="vertical"
                    android:padding="@dimen/space_8"
                    android:visibility="gone"
                    android:id="@+id/ll_header">

                    <TextView
                        android:id="@+id/tv_title"
                        android:layout_width="match_parent"
                        android:layout_height="wrap_content"
                        android:textColor="@color/main_black"
                        android:textSize="@dimen/text_size_18"
                        android:textStyle="bold" />

                    <LinearLayout
                        android:layout_width="match_parent"
                        android:layout_height="wrap_content"
                        android:layout_marginTop="@dimen/space_4"
                        android:gravity="center_vertical"
                        android:orientation="horizontal" >

                        <TextView
                            android:id="@+id/tv_time"
                            android:layout_width="wrap_content"
                            android:layout_height="wrap_content"
                            android:gravity="left|center_vertical"
                            android:textColor="@color/main_gray"
                            android:textSize="@dimen/text_size_12" />

                        <TextView
                            android:id="@+id/tv_source"
                            android:layout_width="wrap_content"
                            android:layout_height="wrap_content"
                            android:layout_marginLeft="@dimen/space_10"
                            android:clickable="true"
                            android:textColor="@color/lightblue"
                            android:textSize="@dimen/text_size_12" />

                    </LinearLayout>
                </LinearLayout>

                <WebView
                    android:id="@+id/webview"
                    android:layout_width="fill_parent"
                    android:layout_height="wrap_content" />
            </LinearLayout>
        </ScrollView>

看下executeOnLoadDataSuccess(T detail)中具體的載入代碼:

        mWebView.loadDataWithBaseURL("", this.getWebViewBody(detail), "text/html", "UTF-8", "");
        // 顯示存儲的字體大小
        mWebView.loadUrl(FontSizeUtils.getSaveFontSize());

使用loadDataWithBaseURL直接加載數(shù)據(jù),而這里的數(shù)據(jù)detail來源有兩種:一是緩存文件中的數(shù)據(jù),二是網(wǎng)絡(luò)請求返回的數(shù)據(jù)。getWebViewBody由子類實(shí)現(xiàn)。
下一行則是使用javascript代碼進(jìn)行文本的字號大小控制了。

    public static String getSaveFontSize() {
        return getFontSize(getSaveFontSizeIndex());
    }

    public static String getFontSize(int fontSizeIndex) {
        String fontSize = "";
        switch (fontSizeIndex) {
            case 0:
                fontSize = "javascript:showSuperBigSize()";
                break;
            case 1:
                fontSize = "javascript:showBigSize()";
                break;
            case 2:
                fontSize = "javascript:showMidSize()";
                break;
            default:
                fontSize = "javascript:showSmallSize()";
                break;
        }
        return fontSize;
    }

js函數(shù)的代碼在assets/detail_page.js中,如:

function showBigSize() {
    var myBody = document.getElementById('article_body');
    myBody.style.fontSize="22px";
}

至于更清晰的調(diào)用路徑,首先,UIHelper 有定義該路徑字串:

public class UIHelper {

    /** 全局web樣式 */
    // 鏈接樣式文件,代碼塊高亮的處理
    public final static String linkCss = "<script type=\"text/javascript\" src=\"file:///android_asset/shCore.js\"></script>"
            + "<script type=\"text/javascript\" src=\"file:///android_asset/brush.js\"></script>"
            + "<script type=\"text/javascript\" src=\"file:///android_asset/client.js\"></script>"
            + "<script type=\"text/javascript\" src=\"file:///android_asset/detail_page.js\"></script>" // This line
            + "<script type=\"text/javascript\">SyntaxHighlighter.all();</script>"
            + "<script type=\"text/javascript\">function showImagePreview(var url){window.location.url= url;}</script>"
            + "<link rel=\"stylesheet\" type=\"text/css\" href=\"file:///android_asset/shThemeDefault.css\">"
            + "<link rel=\"stylesheet\" type=\"text/css\" href=\"file:///android_asset/shCore.css\">"
            + "<link rel=\"stylesheet\" type=\"text/css\" href=\"file:///android_asset/css/common.css\">";

    public final static String WEB_STYLE = linkCss;

然后在子類中進(jìn)行HTML文本構(gòu)造時(shí)會對其進(jìn)行加載:

body.append(UIHelper.WEB_STYLE).append(UIHelper.WEB_LOAD_IMAGES);

上面這句接著看后面的分析你就會看到的。

另外比較關(guān)鍵的部分是緩存添加與讀取,其他則是添加收藏、提示登錄之類的業(yè)務(wù)相關(guān)流程。

NewsDetailFragment extends CommonDetailFragment<News>

設(shè)置獨(dú)特的CacheKey、MobileAPI調(diào)用、數(shù)據(jù)解析等:

@Override
    protected String getCacheKey() {
        return "news_" + mId;
    }

@Override
    protected void sendRequestDataForNet() {
        OSChinaApi.getNewsDetail(mId, mDetailHeandler);
    }

    @Override
    protected News parseData(InputStream is) {
        return XmlUtils.toBean(NewsDetail.class, is).getNews();
    }

HTML數(shù)據(jù)文本構(gòu)造:

@Override
    protected String getWebViewBody(News detail) {
        StringBuffer body = new StringBuffer();
        body.append(UIHelper.WEB_STYLE).append(UIHelper.WEB_LOAD_IMAGES);
        body.append(ThemeSwitchUtils.getWebViewBodyString());
        // 添加title
        body.append(String.format("<div class='title'>%s</div>", mDetail.getTitle()));
        // 添加作者和時(shí)間
        String time = StringUtils.friendly_time(mDetail.getPubDate());
        String author = String.format("<a class='author' , mDetail.getAuthorId(), mDetail.getAuthor());
        body.append(String.format("<div class='authortime'>%s    %s</div>", author, time));
        // 添加圖片點(diǎn)擊放大支持
        body.append(UIHelper.setHtmlCotentSupportImagePreview(mDetail.getBody()));


        // 更多關(guān)于***軟件的信息
        String softwareName = mDetail.getSoftwareName();
        String softwareLink = mDetail.getSoftwareLink();
        if (!StringUtils.isEmpty(softwareName)
                && !StringUtils.isEmpty(softwareLink))
            body.append(String
                    .format("<div class='oschina_software' style='margin-top:8px;font-weight:bold'>更多關(guān)于: <a href='%s'>%s</a> 的詳細(xì)信息</div>",
                            softwareLink, softwareName));

        // 相關(guān)新聞
        if (mDetail != null && mDetail.getRelatives() != null
                && mDetail.getRelatives().size() > 0) {
            String strRelative = "";
            for (News.Relative relative : mDetail.getRelatives()) {
                strRelative += String.format(
                        "<li><a href='%s' style='text-decoration:none'>%s</a></li>",
                        relative.url, relative.title);
            }
            body.append("<p/><div style=\"height:1px;width:100%;background:#DADADA;margin-bottom:10px;\"/>"
                    + String.format("<br/> <b>相關(guān)資訊</b><ul class='about'>%s</ul>",
                    strRelative));
        }
        body.append("<br/>");
        // 封尾
        body.append("</div></body>");
        return  body.toString();
    }

下方工具欄的話,是使用了一個(gè)ToolbarFragment extends BaseFragment來封裝填充到DetailActivity中去的。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 1. Java基礎(chǔ)部分 基礎(chǔ)部分的順序:基本語法,類相關(guān)的語法,內(nèi)部類的語法,繼承相關(guān)的語法,異常的語法,線程的語...
    子非魚_t_閱讀 34,853評論 18 399
  • 《ilua》速成開發(fā)手冊3.0 官方用戶交流:iApp開發(fā)交流(1) 239547050iApp開發(fā)交流(2) 1...
    葉染柒丶閱讀 11,583評論 0 11
  • ¥開啟¥ 【iAPP實(shí)現(xiàn)進(jìn)入界面執(zhí)行逐一顯】 〖2017-08-25 15:22:14〗 《//首先開一個(gè)線程,因...
    小菜c閱讀 7,389評論 0 17
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 179,351評論 25 708
  • 作者:柴田 貓沒日沒夜地嗜睡,而我成宿成宿地失眠; 有沒有想過,可能是貓偷走了我們的睡眠? × 昨夜,我又失眠了。...
    柴田和球閱讀 682評論 1 3

友情鏈接更多精彩內(nèi)容