分页加载
Meizitu的实现
这个功能的实现跟MobileAPI的返回大有关联,我曾经在学习Meizitu的时候看过其实现,然后练手重构了一下Meizitu。
它的MobileAPI是这样的:
http://www.ourhfuu.com/meizitu.php?max_id=
http://www.ourhfuu.com/meizitu.php?since_id=
max_id是个辅助值,标明你现在所有图片中id最大的那个。分页加载需要用到since_id,当然,max_id用来对since_id进行协助计算。
先看个实例http://www.ourhfuu.com/meizitu.php?since_id=999:
易知,其API返回结果为从998开始到979结束,总共返回20条结果。
不信你可以换成http://www.ourhfuu.com/meizitu.php?since_id=979 再试一下。
客户端那边,Meizitu采用数据库存储+Loader,数据流上为
- 从网络加载数据
- 加载好的数据保存至数据库中
- Loader触发观察者效果,根据数据库swapCursor()更新
因为MobileAPI符合之前看到的特性,所以分页加载数据再保存至数据库时无需考虑存在数据重复的问题,又因为使用了Loader,所以不用进行手动的更新UI显示。
总的来说,这样做法非常简洁明了,但问题就在于数据流还绕了一圈,没有从网络加载的数据返回后就直接更新到UI来得自然。不过实际上也没有什么问题就是了,反正已经使用数据库了,存数据库这一步操作不能省略,而更新到UI的话是需要格式化从网络返回的JSON数据的,直接读数据库读的就是格式化后的数据。
OSCHINA的实现
OSCHINA的API返回不是JSON,而是XML,但这不是关键,关键在于其接口返回的数据有什么特性。
这里MobileAPI的参数比Meizitu多代码更绕用法上也更复杂,最直观的做法可以直接抓包然后改改参数看看效果。
不过,我这边是直接看了看代码分析的,先看综合页中的使用的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);
}
直接就有一个第几页的参数page了,不过倒并不知道是不是跟Meizitu的API具有相同的特性。这也不算问题,后面直接看它怎么操作返回数据就能推断出来了。
经过一番阅读,发现网络数据返回后会先进行使用XStream的Bean解析,然后会变成一个类型为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());
// 注销登陆,密码已经修改,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);
// 判断等于是因为最后有一项是listview的状态
if (mAdapter.getCount() == 1) {
if (needShowEmptyNoData()) {
mErrorLayout.setErrorType(EmptyLayout.NODATA);
} else {
mAdapter.setState(ListBaseAdapter.STATE_EMPTY_ITEM);
mAdapter.notifyDataSetChanged();
}
}
}
步骤非常简单明确
- mCurrentPage==0现在是不是分页的话,数据全部加载上去
if (mCurrentPage == 0) {
mAdapter.clear();
}
- for循环去除现有的数据项,也就是说,要么MobileAPI不具有Meizitu的返回特性,要么就是在某种情况下可能会有重复数据项
for (int i = 0; i < data.size(); i++) {
if (compareTo(mAdapter.getData(), data.get(i))) {
data.remove(i);
i--;
}
}
compareTo的实现也是十分暴力,如果id存在规律的话,倒是可以采用二分之类的改进一下:
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状态,然后添加新的数据项:
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的实现,直接在数据集的末尾追加,然后通知改变即可:
public void addData(List<T> data) {
if (mDatas != null && data != null && !data.isEmpty()) {
mDatas.addAll(data);
}
notifyDataSetChanged();
}
另外
其他的就是一些细节需要进行注意,比如滑动状态的细分处理。
对了,ListView最下面总会有个footerView进来表示“正在加载...”之类,这个footerView的存在导致ListView永不为空,且计算数值时也要考虑它的因素,还有就是各种情况下的显隐性了。
详情
所谓的详情界面,就是从资讯或者博客点击item进入后的Activity:
点击过后的item项灰显了,因为它已被加入了第一篇提到过的已读列表之中。
跳转的代码看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也看过几次了,可以得出结论,该应用在设计上使用这个类作为中间层来统一管理UI切换的操作。
其中针对不同的url情况做了较多的判定封装,就不进行直接研读了。因为最常见的即为上面的截图“资讯详情”与“博客详情”,所以我们直接查找字串然后定位具体实现的Activity即可。
最后发现实现类为这个:
/**
* 详情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
这个类主要封装了黑白主题的设置、ActionBar操作、Toast操作(注意使用的是自定义的一个CommonToast)、ProgressDialog等。
DetailActivity extends BaseActivity
非常容易理解,放了多个标志位来区分进行不同的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;
自顶向下分析吧,因为BaseFragment之前的篇章已分析过,所以从这里开始:
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来实现网页浏览,具有效果表现需要看服务器端的适配情况了。
看布局文件,发现其标题、作者、时间三项是使用原生控件来做的:
<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直接加载数据,而这里的数据detail来源有两种:一是缓存文件中的数据,二是网络请求返回的数据。getWebViewBody由子类实现。
下一行则是使用javascript代码进行文本的字号大小控制了。
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函数的代码在assets/detail_page.js中,如:
function showBigSize() {
var myBody = document.getElementById('article_body');
myBody.style.fontSize="22px";
}
至于更清晰的调用路径,首先,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;
然后在子类中进行HTML文本构造时会对其进行加载:
body.append(UIHelper.WEB_STYLE).append(UIHelper.WEB_LOAD_IMAGES);
上面这句接着看后面的分析你就会看到的。
另外比较关键的部分是缓存添加与读取,其他则是添加收藏、提示登录之类的业务相关流程。
NewsDetailFragment extends CommonDetailFragment<News>
设置独特的CacheKey、MobileAPI调用、数据解析等:
@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数据文本构造:
@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()));
// 添加作者和时间
String time = StringUtils.friendly_time(mDetail.getPubDate());
String author = String.format("<a class='author' href='http://my.oschina.net/u/%s'>%s</a>", mDetail.getAuthorId(), mDetail.getAuthor());
body.append(String.format("<div class='authortime'>%s %s</div>", author, time));
// 添加图片点击放大支持
body.append(UIHelper.setHtmlCotentSupportImagePreview(mDetail.getBody()));
// 更多关于***软件的信息
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'>更多关于: <a href='%s'>%s</a> 的详细信息</div>",
softwareLink, softwareName));
// 相关新闻
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>相关资讯</b><ul class='about'>%s</ul>",
strRelative));
}
body.append("<br/>");
// 封尾
body.append("</div></body>");
return body.toString();
}
下方工具栏的话,是使用了一个ToolbarFragment extends BaseFragment来封装填充到DetailActivity中去的。