分頁加載
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用來對(duì)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ù)庫存儲(chǔ)+Loader游两,數(shù)據(jù)流上為
- 從網(wǎng)絡(luò)加載數(shù)據(jù)
- 加載好的數(shù)據(jù)保存至數(shù)據(jù)庫中
- Loader觸發(fā)觀察者效果,根據(jù)數(shù)據(jù)庫swapCursor()更新
因?yàn)镸obileAPI符合之前看到的特性漩绵,所以分頁加載數(shù)據(jù)再保存至數(shù)據(jù)庫時(shí)無需考慮存在數(shù)據(jù)重復(fù)的問題贱案,又因?yàn)槭褂昧薒oader,所以不用進(jìn)行手動(dòng)的更新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ù)返回后會(huì)先進(jìn)行使用XStream的Bean解析雏节,然后會(huì)變成一個(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();
}
}
}
步驟非常簡單明確
- mCurrentPage==0現(xiàn)在是不是分頁的話辞州,數(shù)據(jù)全部加載上去
if (mCurrentPage == 0) {
mAdapter.clear();
}
- for循環(huán)去除現(xiàn)有的數(shù)據(jù)項(xiàng),也就是說寥粹,要么MobileAPI不具有Meizitu的返回特性变过,要么就是在某種情況下可能會(huì)有重復(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;
}
- 置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)行注意崭孤,比如滑動(dòng)狀態(tài)的細(xì)分處理。
對(duì)了衫生,ListView最下面總會(huì)有個(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切換的操作。
其中針對(duì)不同的url情況做了較多的判定封裝拓轻,就不進(jìn)行直接研讀了斯撮。因?yàn)樽畛R姷募礊樯厦娴慕貓D“資訊詳情”與“博客詳情”,所以我們直接查找字串然后定位具體實(shí)現(xiàn)的Activity即可扶叉。
最后發(fā)現(xiàn)實(shí)現(xiàn)類為這個(gè):
/**
* 詳情activity(包括:資訊勿锅、博客、軟件枣氧、問答溢十、動(dòng)彈)
*
* @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", "");
// 顯示存儲(chǔ)的字體大小
mWebView.loadUrl(FontSizeUtils.getSaveFontSize());
使用loadDataWithBaseURL直接加載數(shù)據(jù),而這里的數(shù)據(jù)detail來源有兩種:一是緩存文件中的數(shù)據(jù)酗电,二是網(wǎng)絡(luò)請(qǐng)求返回的數(shù)據(jù)魄藕。getWebViewBody由子類實(shí)現(xiàn)。
下一行則是使用javascript代碼進(jìn)行文本的字號(hào)大小控制了撵术。
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í)會(huì)對(duì)其進(jìn)行加載:
body.append(UIHelper.WEB_STYLE).append(UIHelper.WEB_LOAD_IMAGES);
上面這句接著看后面的分析你就會(huì)看到的。
另外比較關(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中去的同窘。