Deprecated!

更好的实现方式: 使用 android.support.design.widget.CoordinatorLayout.


本文详细介绍如何实现如下图中的微博正文页面效果, 其中包括:

> 实现页面滚动时[转发-评论-赞]工具条的Sticky悬停效果

> 实现工具条切换, 并解决[转发-评论-赞]子页面item不足以占满屏幕时切换页面导致屏幕滚动的问题

知识要点:

  1. ListView

  2. ListView # HeaderView & FooterView

  3. AbsListView.onScrollListener

  4. ListAdapter

实现代码:

> 定义

  • DetailActivity - 正文界面
  • MiddleTab - 正文界面中的工具条

> 界面需求-1

  首先实现的是Sticky悬停效果, 基本思路是:

  1. 设计正文界面的Layout-XML布局, 并以ListView来展示转发/评论/赞列表的详情.
  2. 设计MiddleTab的Layout-XML布局, 以android:visibility="gone"方式添加到上一个Layout-XML中, 这里的MiddleTab, 我们暂且叫它为Main-MiddleTab
  3. 设计另外一个布局, 包含正文布局和MiddleTab-Layout-XML, 并把它作为ListView的HeaderView, 我们暂且叫它为HeaderView-MiddlerTab
  4. 当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;}@Overridepublic 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)*/@Overridepublic 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;@Overrideprotected 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);}@Overridepublic 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 会使用到它
        }}
    @Overridepublic 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;}}@Overridepublic 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);}
        @Overridepublic 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();}@Overridepublic boolean hasStableIds() {return (mAdapter != null) ? mAdapter.hasStableIds() : false;}@Overridepublic boolean isEmpty() {return mAdapter == null || mAdapter.isEmpty();}@Overridepublic int getCount() {int adapterCount = (mAdapter != null) ? mAdapter.getCount() : 0;return getPlaceholdersCount() + adapterCount;}@Overridepublic boolean areAllItemsEnabled() {if (mAdapter != null) {return mAdapter.areAllItemsEnabled();} else {return true;}}@Overridepublic boolean isEnabled(int position) {if (mAdapter != null && position < mAdapter.getCount()) {return mAdapter.isEnabled(position);}return false;}@Overridepublic 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;}@Overridepublic long getItemId(int position) {if (mAdapter != null && position < mAdapter.getCount()) {return mAdapter.getItemId(position);}return -1;}@Overridepublic 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;}@Overridepublic int getItemViewType(int position) {if (mAdapter != null && position < mAdapter.getCount()) {return mAdapter.getItemViewType(position);}return AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER;}@Overridepublic int getViewTypeCount() {return (mAdapter != null) ? mAdapter.getViewTypeCount() : 1;}@Overridepublic void registerDataSetObserver(DataSetObserver observer) {if (mAdapter != null) {mAdapter.registerDataSetObserver(observer);}}@Overridepublic void unregisterDataSetObserver(DataSetObserver observer) {if (mAdapter != null) {mAdapter.unregisterDataSetObserver(observer);}}@Overridepublic 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.

转载于:https://www.cnblogs.com/erehmi/p/4225501.html

[Deprecated!] Android开发案例 - 微博正文相关推荐

  1. 《Android开发案例驱动教程》

    <Android开发案例驱动教程> 作者:关东升,赵志荣 Java或C++程序员转变成为Android程序员 采用案例驱动模式展开讲解知识点,即介绍案例->案例涉及技术->展开 ...

  2. android开发案例

    20 多个可以提高你安卓开发技能的开源 app 学习的最佳方式就是阅读,对程序员来说也是如此.如果你想成为一个更优秀的程序员,你必须的代码,就是这么简单.书籍,博客,论坛在某种程度上都是有益的,但是没 ...

  3. Android开发案例教程吴志祥,Android应用开发案例教程(Android Studio版)

    本书内容浅显易懂,可操作性强.全书共分9章,第1-7章详细介绍了Android Studio基础知识,包括Android UI设计.Activity与多个用户界面.多媒体播放与录制.广播与服务.数据存 ...

  4. Android开发案例Onclick点击事件switch调用分类04

    第一步:设置sting.xml中配置Button. <resources><string name="app_name">04Onclicks</st ...

  5. Android开发案例之电话拨号器

    原理: 调用android系统的拨号功能实现拨号. 常用场景:APP中看到号码就点击直接拨打号码. 开发过程: 1. new 一个android项目 2.拨号界面activity_main.xml: ...

  6. php mysql 开发微博_php+mysql基于Android的手机微博应用开发

    摘要:本系统采用Eclipse作为开发工具,数据库基于MySQL,服务器的编写使用的是PHP语言,开发了基于Android平台开的C/S模式的手机微博系统.系统从符合操作简便.界面友好.使用灵活.实用 ...

  7. 《Android 应用案例开发大全(第二版)》——2.6节绘制相关类

    本节书摘来自异步社区<Android 应用案例开发大全(第二版)>一书中的第2章,第2.6节绘制相关类 ,作者 吴亚峰 , 于复兴 , 杜化美,更多章节内容可以访问云栖社区"异步 ...

  8. 《Android 应用案例开发大全(第二版)》——6.1节Android系统的信使:Intent

    本节书摘来自异步社区<Android 应用案例开发大全(第二版)>一书中的第6章,第6.1节Android系统的信使:Intent ,作者李宁,更多章节内容可以访问云栖社区"异步 ...

  9. android 对称加密和非对称加密,Android开发加密之对称与非对称加密算法使用案例.pdf...

    Android开发加密之对称与非对称加密算法使用案例 消息摘要 md5:登录注册, sha1 对称加密  1.des:Data Encryption Standard,数据加密标准  2.aes: ...

  10. 《Android 应用案例开发大全(第二版)》——导读

    本节书摘来自异步社区<Android 应用案例开发大全(第二版)>一书中的目录 ,作者 吴亚峰 , 于复兴 , 杜化美,更多章节内容可以访问云栖社区"异步社区"公众号查 ...

最新文章

  1. luogu P6113 【模板】一般图最大匹配(带花树)
  2. 阿里技术人的成长路径是什么?
  3. Orchard模块开发全接触3:分类的实现及内容呈现(Display)
  4. Android studio libs目录
  5. Python之面向对象继承和派生
  6. silverlight + wcf(json格式) + sqlserver存储过程分页
  7. C++: 06---构造函数析构函数
  8. 【ASM C/C++】 Makefile 规则说明
  9. Visual Studio .NET已检测到指定的Web服务器运行的不是ASP.NET 1.1 版...的解决办法
  10. mknod 创建内核设备文件【原创】
  11. [转载]VC6中的文件后缀
  12. 【Bug】下载steam游戏的E盘莫名其妙爆满
  13. 鼠标滚轮控制页面滚动(山寨苹果官网iPhone5s的滚屏动画实例)
  14. 【牛腩】未能加载文件或程序集“AspNetPager”或它的某一个依赖项
  15. 我看到左岸读书上的留言,感觉挺有新意
  16. 碗中有米,心中有他,他解决的不只是吃饭问题......
  17. html onload不起作用,body标签onLoad执行无效,为何?
  18. MMDetection框架入门教程(三):配置文件详细解析
  19. 如何通过QA质量管理提高软件质量?
  20. 冬季盲目补农夫山蒸营养易引发青春痘

热门文章

  1. 华为服务器通过ilo虚拟光驱,如何通过ilo开启服务器远程桌面
  2. python用lda主题_python下进行lda主题挖掘(二)——利用gensim训练LDA模型
  3. NAS个人云存储服务器搭建
  4. java cmd 编译jar_Java程序在命令行下编译运行打Jar包
  5. Rufus制作Ubuntu18.04启动盘并为Dell电脑重装系统
  6. UBI及EXT4文件系统
  7. 防御SQL注入的方法总结
  8. .log 合并或 .txt 合并
  9. FreeMarker模板引擎实现页面静态化
  10. 《Redis视频教程》(p5)