[Deprecated!] Android开发案例 - 微博正文
Deprecated!
更好的实现方式: 使用 android.support.design.widget.CoordinatorLayout.
本文详细介绍如何实现如下图中的微博正文页面效果, 其中包括:
> 实现页面滚动时[转发-评论-赞]工具条的Sticky悬停效果
> 实现工具条切换, 并解决[转发-评论-赞]子页面item不足以占满屏幕时切换页面导致屏幕滚动的问题

知识要点:
ListView
ListView # HeaderView & FooterView
AbsListView.onScrollListener
ListAdapter
实现代码:
> 定义
- DetailActivity - 正文界面
- MiddleTab - 正文界面中的工具条
> 界面需求-1
首先实现的是Sticky悬停效果, 基本思路是:
- 设计正文界面的Layout-XML布局, 并以ListView来展示转发/评论/赞列表的详情.
- 设计MiddleTab的Layout-XML布局, 以android:visibility="gone"方式添加到上一个Layout-XML中, 这里的MiddleTab, 我们暂且叫它为Main-MiddleTab
- 设计另外一个布局, 包含正文布局和MiddleTab-Layout-XML, 并把它作为ListView的HeaderView, 我们暂且叫它为HeaderView-MiddlerTab
- 当ListView#HeaderView中的MiddleTab滚出屏幕顶部时, 显示Main-MiddleTab
进入正题前, 先介绍以下几个类和变量:
- [类] ScrollDetector - ListView滚动的辅助类, 它用来监听第一个Item滚动事件以及最后一个Item显示事件, 代码如下.
- [类] HdrViewHolder - 用保存HeaderView的Holder容器
- [变量] HdrViewHolder.middleTabs - HeaderView中的工具条
- [变量] mMiddleTabs - Main-MiddleTab
- [变量] mListView - 用来展示正文及详情的ListView
- [变量] mListAdapter - ListItem适配器, 在这里, 我们假设它为DetailsAdapter类型(支持ArrayAdapter的操作), 并假定它能完美支持转发/评论/赞列表(本文中将不实现DetailsAdapter代码, 因为它的代码实现和常规Adapter基本相同).
- [变量] R.id.*Tab - MiddleTab中转发/评论/赞所对应的RadioButton-id
import ... /**
* Detect scroll events of list or grid.
*/
public class ScrollDetector implements OnScrollListener {
/** @see #onScroll(android.widget.AbsListView, int, int, int) */
private boolean mFirstItemVisible = false;
private OnFirstItemScrollListener mFisListener;
private OnLastItemVisibleListener mLivListener; public ScrollDetector(OnFirstItemScrollListener fisListener,
OnLastItemVisibleListener livListener) {
mFisListener = fisListener;
mLivListener = livListener;
} public void setOnFirstItemScrollListener(OnFirstItemScrollListener listener) {
mFisListener = listener;
} public void setOnLastItemVisibleListener(OnLastItemVisibleListener listener) {
mLivListener = listener;
} @Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
if (mLivListener != null) {
if (triggerLastItemVisible(view, scrollState)) {
mLivListener.onLastItemVisible();
}
}
} private boolean triggerLastItemVisible(AbsListView view, int scrollState) {
return (scrollState == SCROLL_STATE_IDLE &&
(view.getLastVisiblePosition() == view.getCount() - 1));
} /**
* 用超高的初速度滚动AbsListView时, 可能会出现跳过firstVisibleItem=0的情况, 因此,
* 通过设置mFirstItemVisible来避免在出现上述情况时不会调用onFirstItemScroll的问题
*
* @see OnScrollListener#onScroll(android.widget.AbsListView, int, int, int)
*/
@Override
public void onScroll(AbsListView view, int firstVisibleItem,
int visibleItemCount, int totalItemCount) {
if (mFisListener != null) {
if (triggerFirstItemScroll(view, firstVisibleItem) || mFirstItemVisible) {
mFisListener.onFirstItemScroll(view.getChildAt(0));
if (!mFirstItemVisible) {
mFirstItemVisible = true;
}
} else {
if (mFirstItemVisible) {
mFirstItemVisible = false;
}
}
}
} private boolean triggerFirstItemScroll(AbsListView view, int firstVisibleItem) {
return (firstVisibleItem == 0);
} public static interface OnLastItemVisibleListener {
void onLastItemVisible();
} public static interface OnFirstItemScrollListener {
void onFirstItemScroll(View itemView);
}
}
ScrollDetector.java
Layout-XML代码略. 需要说明的是, 工具条是以RadioGroup方式实现的. 以下为DetailActivity.java代码:
import ...
public class DetailActivity extends Activity implements onClickListener, OnCheckedChangeListener, OnFirstItemScrollListener {
...
private ListView mListView;
private DetailsAdapter mListAdapter;
private RadioGroup mMiddleTabs;
private HdrViewHolder mHdrViewHolder;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_detail);
...
//initMiddleTabsStates(); // 暂时不用理会这行代码, 界面需求-2 会使用到它
initContent();
initDetails();
}
private void initContent() {
final LayoutInflater inflater = getLayoutInflater();
View hdrView = inflater.inflate(R.layout.item_detail_header, mListView, false);
mHdrViewHolder = new HdrViewHolder(hdrView);
mListView.addHeaderView(hdrView);
...
mHdrViewHolder.middleTabs.check(R.id.commentTab);
}
private void initDetails() {
mScrollDetector = new ScrollDetector(this, this);
mListView.setOnScrollListener(mScrollDetector);
mListAdapter = new DetailsAdapter(this);
mListView.setAdapter(wrapperAdapter);
mMiddleTabs.check(R.id.commentTab);
}
@Override
public void onClick(View v) {
int id = buttonView.getId();
switch(id) {
case R.id.forwardTab:
case R.id.commentTab:
case R.id.praiseTab:
if (!checked) {// 切换TAB前保存当前CommentTab的状态
updateMiddleTabs(id);
}
break;
}
}
private void updateMiddleTabs(int id) {
if (mMiddleTabs.getCheckedRadioButtonId() == id) {
mHdrViewHolder.middleTabs.check(id);
//restoreMiddleTabsStates();// 暂时不用理会这行代码, 界面需求-2 会使用到它
}
}
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
int id = buttonView.getId();
switch(id) {
case R.id.forwardTab:
case R.id.commentTab:
case R.id.praiseTab:
if (!checked) {// 切换TAB前保存当前CommentTab的状态
//saveMiddleTabsStates(id); // 暂时不用理会这行代码, 界面需求-2 会使用到它
}
break;
}
}
@Override
public void onFirstItemScroll(View itemView) {
int[] location = new int[2];
int[] location2 = new int[2];
mHdrViewHolder.middleTabs.getLocationOnScreen(location);
mMiddleTabs.getLocationOnScreen(location2);
boolean visible = (location[1] <= location2[1]);
mMiddleTabs.setVisibility(visible ? View.VISIBLE : View.GONE);
}
static class HdrViewHolder implements onClickListener {
...
@InjectView(R.id.tabs)
RadioGroup middleTabs;
HdrViewHolder(View view) {
...
middleTabs.setOnClickListener(this);
}
@Override
public void onClick(View v) {
int id = buttonView.getId();
switch(id) {
case R.id.forwardTab:
case R.id.commentTab:
case R.id.praiseTab:
if (!checked) {// 切换TAB前保存当前CommentTab的状态
updateMiddleTabs(id);
}
break;
}
}
private void updateMiddleTabs(int id) {
if (middleTabs.getCheckedRadioButtonId() == id) {
mMiddleTabs.check(id);
//restoreMiddleTabsStates();// 暂时不用理会这行代码, 界面需求-2 会使用到它
}
}
}
}
在上述代码中, 首先用ScrollDetector实现了Sticky悬停效果, 然后就是同步Main-MiddleTab和HeaderView-MiddlerTab的checked状态. 接下来, 再看如何实现界面需求-2.
> 界面需求-2
进入正题前, 我们还得介绍一个辅助类PlaceholderListAdapter, 它虽然有点像android系统的HeaderViewListAdapter, 但它却是我们用来应付Item未能占满ListView的情况的辅助类. 假想下当所有数据都加载到ListView的情况, 如果第一个数据项已经不显示在ListView上, 那么这时我们可以认为ListView已经被Item占满了, 否则, 就需要非数据项视图或者FooterView来占满空余的ListView. 而PlaceholderListAdapter的原理正是这样, 我们先预置一个类似FooterView的View给PlaceholderListAdapter, 并且在getView()时检测ListView是否已经需要显示该View了, 如果是, 则按上述逻辑来处理. 代码如下:
import ... /**
* PlaceholderListAdapter可以帮助我们解决这样的问题:<br> <ul><li>当所有Item视图不足以占满ListView时,
* 用空白视图来填充空白区域.</li></ul><br> 效果图见微博Android客户端的微博正文页面. 该适配器主要是用来提升用户体验的,
* 尤其是在切换TAB时.
*
* @see android.widget.HeaderViewListAdapter
*/
public class PlaceholderListAdapter implements WrapperListAdapter {
private final ListAdapter mAdapter; public class FixedViewInfo {
public View view;
public Object data;
public boolean isSelectable;
} private ArrayList<FixedViewInfo> mFooterViewInfos;
private View mPinnedHeaderView; public PlaceholderListAdapter(Context context, ListAdapter adapter) {
mAdapter = adapter;
mFooterViewInfos = new ArrayList<FixedViewInfo>();
init(context);
} private void init(Context context) {
View placeholder = new View(context);
placeholder.setLayoutParams(
new AbsListView.LayoutParams(
LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));
addFooterView(placeholder, true);
} public void setPinnedHeaderView(View view) {
mPinnedHeaderView = view;
} public void addFooterView(View view) {
addFooterView(view, false);
} private void addFooterView(View view, boolean isPlaceholder) {
FixedViewInfo info = new FixedViewInfo();
info.view = view;
info.data = null;
info.isSelectable = true;
if (isPlaceholder) {
mFooterViewInfos.add(info);
} else {
mFooterViewInfos.add(mFooterViewInfos.size() - 1, info);
}
} public int getPlaceholdersCount() {
return mFooterViewInfos.size();
} @Override
public boolean hasStableIds() {
return (mAdapter != null) ? mAdapter.hasStableIds() : false;
} @Override
public boolean isEmpty() {
return mAdapter == null || mAdapter.isEmpty();
} @Override
public int getCount() {
int adapterCount = (mAdapter != null) ? mAdapter.getCount() : 0;
return getPlaceholdersCount() + adapterCount;
} @Override
public boolean areAllItemsEnabled() {
if (mAdapter != null) {
return mAdapter.areAllItemsEnabled();
} else {
return true;
}
} @Override
public boolean isEnabled(int position) {
if (mAdapter != null && position < mAdapter.getCount()) {
return mAdapter.isEnabled(position);
}
return false;
} @Override
public Object getItem(int position) {
int adapterCount = 0;
if (mAdapter != null) {
adapterCount = mAdapter.getCount();
if (position < adapterCount) {
return mAdapter.getItem(position);
}
} return mFooterViewInfos.get(position - adapterCount).data;
} @Override
public long getItemId(int position) {
if (mAdapter != null && position < mAdapter.getCount()) {
return mAdapter.getItemId(position);
} return -1;
} @Override
public View getView(int position, View convertView, ViewGroup parent) {
int adapterCount = 0;
if (mAdapter != null) {
adapterCount = mAdapter.getCount();
if (position < adapterCount) {
return mAdapter.getView(position, convertView, parent);
}
} View view = mFooterViewInfos.get(position - adapterCount).view;
if (position == getCount() - 1) {// 当convertView为占位View时
if (!(parent instanceof ListView)) {
throw new IllegalArgumentException("the parent is not a ListView.");
} ListView listView = (ListView) parent;
int startPosition = listView.getHeaderViewsCount();
int itemsHeight = (mPinnedHeaderView != null) ? mPinnedHeaderView.getHeight() : 0;
int firstVisiblePos = listView.getFirstVisiblePosition();
int lastVisiblePos = listView.getLastVisiblePosition();
if (startPosition >= firstVisiblePos) {// 第一个数据视图还在屏幕上, 此时需要占位视图
for (int i = startPosition; i <= lastVisiblePos; ++i) {
View childView = listView.getChildAt(i - firstVisiblePos);
itemsHeight += childView.getHeight();
}
} else {// 第一个数据视图已经滚出屏幕, 此时不需要显示占位视图
itemsHeight = listView.getHeight();
} ViewGroup.LayoutParams params = view.getLayoutParams();
if (params == null) {
throw new IllegalArgumentException("the layout parameters is not set.");
} params.height = listView.getHeight() - itemsHeight;
//view.setLayoutParams(params);
} return view;
} @Override
public int getItemViewType(int position) {
if (mAdapter != null && position < mAdapter.getCount()) {
return mAdapter.getItemViewType(position);
}
return AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER;
} @Override
public int getViewTypeCount() {
return (mAdapter != null) ? mAdapter.getViewTypeCount() : 1;
} @Override
public void registerDataSetObserver(DataSetObserver observer) {
if (mAdapter != null) {
mAdapter.registerDataSetObserver(observer);
}
} @Override
public void unregisterDataSetObserver(DataSetObserver observer) {
if (mAdapter != null) {
mAdapter.unregisterDataSetObserver(observer);
}
} @Override
public ListAdapter getWrappedAdapter() {
return mAdapter;
}
}
PlaceholderListAdapter.java
既然有了PlaceholderListAdapter, 那后面就是很简单的事情了, 就只剩下在切换MiddleTab时保存ListView的滚动位置的问题了, 到这里, 就可以用到上面那些默默占位, 却没被理会的代码了. 代码如下:
private static class SavedState {
private int viewTop;
private int position;
private int loadState;
private List<Object> objects;
private SavedState() {
position = -1;
viewTop = 0;
objects = new ArrayList<Object>();
loadState = LOAD_STATE_IDLE;
}
}
private SparseArray<SavedState> mSavedStates;
private void initMiddleTabsStates() {
mSavedStates = new SparseArray<SavedState>();
int[] ids = {R.id.commentTab, R.id.praiseTab};
for (int id : ids) {
mSavedStates.put(id, new SavedState());
}
}
private void initDetails() {
mScrollDetector = new ScrollDetector(this, this);
mListView.setOnScrollListener(mScrollDetector);
mListAdapter = new DetailsAdapter(this);
//mListView.setAdapter(wrapperAdapter); // 这行代码只能满足界面需求-1
PlaceholderListAdapter wrapperAdapter = new PlaceholderListAdapter(this, mListAdapter);
wrapperAdapter.setPinnedHeaderView(mHdrViewHolder.middleTabs);
View footerView = getLayoutInflater().inflate(R.layout.load_more, mListView, false);
wrapperAdapter.addFooterView(footerView);
//wrapperAdapter.addPlaceholder(mPlaceholder);
mListView.setAdapter(wrapperAdapter);
mMiddleTabs.check(R.id.commentTab);
}
/**
* 还原对应ID的TAB的状态
*
* @see #saveMiddleTabsStates(int)
*/
private void restoreMiddleTabsStates() {
int id = mMiddleTabs.getCheckedRadioButtonId();
SavedState state = mSavedStates.get(id);
mListAdapter.setNotifyOnChange(false);
mListAdapter.clear();
mListAdapter.addAll(state.objects);
mListAdapter.notifyDataSetChanged();
if (mMiddleTabs.getVisibility() == View.VISIBLE) {
if (state.position == -1) {
setPinnedSavedState(state);
}
mListView.setSelectionFromTop(state.position, state.viewTop);
}
}
/**
* 保存对应ID的TAB的状态, 并在切换回来之后, 还原该TAB的状态
*
* @see #restoreMiddleTabsStates()
*/
private void saveMiddleTabsStates(int id) {
SavedState state = mSavedStates.get(id);
if (mMiddleTabs.getVisibility() == View.VISIBLE) {
View child = mListView.getChildAt(0);
int viewTop = ((child != null) ? (child.getTop() - mListView.getPaddingTop()) : 0);
state.position = mListView.getFirstVisiblePosition();
state.viewTop = viewTop;
} else {
setPinnedSavedState(state);
}
}
private void setPinnedSavedState(SavedState state) {
state.position = mListView.getHeaderViewsCount();
state.viewTop = mMiddleTabs.getHeight() - mListView.getPaddingTop();
}
到这里, 我们已经实现了微博正文界面. 需要说明的是使用PlaceholderListAdapter之后, 建议用PlaceholderListAdapter.addFooterView(View view)来替代ListView.addFooterView(View view)调用, 否则会在ListView的Item与FooterView之间出现空白区域, 那正是我们用来占满空余ListView的视图.
END.
[Deprecated!] Android开发案例 - 微博正文的更多相关文章
- AllJoyn+Android开发案例-android跨设备调用方法
AllJoyn+Android开发案例-android跨设备调用方法 项目须要涉及AllJoyn开源物联网框架.前面主要了解了一些AllJoyn主要的概念.像总线,总线附件,总线对象,总线接口这种概念 ...
- Android开发案例 - 自定义虚拟键盘
所有包含IM功能的App(如微信, 微博, QQ, 支付宝等)都提供了Emoji表情之类的虚拟键盘, 如下图: 本文只着重介绍如何实现输入法键盘和自定义虚拟键盘的流畅切换, 而不介绍如何实现虚 ...
- Android开发案例 - 图库
本文不涉及UI方面的内容, 如果您是希望了解UI方面的访客, 请跳过此文. 本文将要详细介绍如何实现流畅加载本地图库. 像平时用得比较多应用, 如微信(见下图), 微博等应用, 都实现了图库功能, 其 ...
- Android开发案例 - 欢迎界面
本文详细描述了如何实现如下图中的微信启动界面. 该类启动界面的特点是在整个Application的生命周期里, 它只会出现在第一次进入应用时, 即便按回退键到桌面之后. 使用该类启动界面的应用还有: ...
- Android开发案例 设置背景图片轮播
点击按钮实现图片轮播效果 实践案例: xml <?xml version="1.0" encoding="utf-8"?> <LinearLa ...
- Android开发案例 – 在AbsListView中使用倒计时
在App中, 有多种多样的倒计时需求, 比如: 在单View上, 使用倒计时, 如(如图-1) 在ListView(或者GridView)的ItemView上, 使用倒计时(如图-2) 图-1 图-2 ...
- Android开发案例 - 淘宝商品详情
所有电商APP的商品详情页面几乎都是和淘宝的一模一样(见下图): 采用上下分页的模式 商品基本参数 & 选购参数在上页展示 商品图文详情等其他信息放在下页展示 知识要点 垂直方向的ViewPa ...
- Android开发案例 - 注册登录
本文只涉及UI方面的内容, 如果您是希望了解非UI方面的访客, 请跳过此文. 在微博, 微信等App的注册登录过程中有这样的交互场景(如下图): 打开登录界面 在登录界面中, 点击注册, 跳转到注册界 ...
- Android开发案例 - 淘宝商品详情【转】
http://erehmi.cnblogs.com/ 所有电商APP的商品详情页面几乎都是和淘宝的一模一样(见下图): 采用上下分页的模式 商品基本参数 & 选购参数在上页展示 商品图文详情等 ...
随机推荐
- 一年之计在于春,2015开篇:PDF.NET SOD Ver 5.1完全开源
前言: 自从我2014年下半年到现在的某电商公司工作后,工作太忙,一直没有写过一篇博客,甚至连14年股票市场的牛市都错过了,现在马上要过年了,而今天又是立春节气,如果再不动手,那么明年这个无春的年,也 ...
- 新手如何在gdb中存活
网络上已经有很多gdb调试的文章了,为什么我还要写这篇文章呢,因为本文是写给gdb新手的,目的就是通过一个简单的例子来让新手很快上手.一旦上手入门了,其他的问题就可以自己去搜索搞定了.右边是gdb的L ...
- 玩转Windows服务系列——Debug、Release版本的注册和卸载,及其原理
Windows服务Debug版本 注册 Services.exe -regserver 卸载 Services.exe -unregserver Windows服务Release版本 注册 Servi ...
- 创建DbContext
返回总目录<一步一步使用ABP框架搭建正式项目系列教程> 上一篇介绍了<创建实体>,这一篇我们顺其自然地介绍<创建DbContext>. 温故: 提到DbConte ...
- ABP框架 - 值对象
文档目录 本节内容: 简介 值对象基类 最佳实践 简介 “一个表示领域的一个描述性方面的没有概念上的身份对象,称为值对象.“(Eric Evans). 与一个有身份(Id)实体相反,一个值对象没有身份 ...
- Java 设计模式 —— 单例模式
1. 概念: 单例模式是一种常用的软件设计模式.核心结构中只包含一个被称为单例的特殊类.通过单例模式可以保证系统中一个类只有一个实例而且该实例易于外界访问,从而方便对实例个数的控制并节约系统资源.如果 ...
- Android okHttp网络请求之Json解析
前言: 前面两篇文章介绍了基于okHttp的post.get请求,以及文件的上传下载,今天主要介绍一下如何和Json解析一起使用?如何才能提高开发效率? okHttp相关文章地址: Android o ...
- Log4net入门(控制台篇)
Log4net是Apache公司的log4j™的.NET版本,用于帮助.NET开发人员将日志信息输出到各种不同的输出源(Appender),常见的输出源包括控制台.日志文件和数据库等.本篇主要讨论如何 ...
- 设计模式(八): 从“小弟”中来类比"外观模式"(Facade Pattern)
在此先容我拿“小弟”这个词来扯一下淡.什么是小弟呢,所谓小弟就是可以帮你做一些琐碎的事情,在此我们就拿“小弟”来类比“外观模式”.在上面一篇博文我们完整的介绍了“适配器模式”,接下来我们将要在这篇博客 ...
- 一个技术汪的开源梦 —— 基于 .Net Core 的公共组件之目录结构
一个技术汪的开源梦 —— 目录 这篇文章是开源公共组件的开篇那就先说说项目的 Github 目录结构和 .Net Core 的项目结构. 1. GitHub 目录结构和相关文件 - src 源码项目目 ...