开源中国源码地址:http://git.oschina.net/oschina/android-app
咨讯详情页面分析
布局页面文件fragment_general_news_detail.xml布局文件中包含上滑评论条隐藏,下滑评论条显示
相关推荐、相关评论布局是通过继承LinearLayout再填充marge布局页面
相关推荐布局文件lay_detail_about_layout.xml,看运行效果貌似是ListView,
其实是LineartLayout,由DetailAboutView类的如下代码实现
详情页面文件NewsDetailActivity.java其继承于DetailAvtivity.java,DetailActivity.java的布局文件中帧布局父容器包含一个帧布局和一个自定义的EmptyLayout。基类中onCreate()方法中的initWindow()、initWidget()、initData();getContentView()、getImageLoader()
public abstract class DetailActivity<Data, DataView extends DetailContract.View> extends BaseBaceActivity implements DetailContract.Operator<Data, DataView> 有2个泛型的属性
private Data mData;
private DataView mView;
数据的加载是在NewsDetailActivity.java文件中,数据的展示是在NewsDetailFragment.java文件中
在DetailFragment.java的如下代码关联了数据的获取和数据的展示
public void onAttach(Context context) {
this.mOperator = (Operator) context;
this.mOperator.setDataView((DataView) this);
super.onAttach(context);
}
public interface DetailContract 、 public interface NewsDetailContract 同时被NewsDetaiActivity 和 NewsDetailFragment实现
数据在NewsDetailActivity.java文件中请求成功后才会通过handleView()方法填充NewsDetailFragment.java文件
NewsDetailFragment.java
CommentsView.java
对于评论中的他人跟随的评论CommentsUtil.java
综合 -> 资讯 -> 博客详情页面 底部评论工具条上滑消失,下滑出现的实现方式:
1. 根布局是CoordinatorLayout
2. 上面滑动布局的根布局是NestedScrollView
3. 在底部评论工具条的容器布局中添加一个属性app:layout_behavior="FloatingHideDownBehavior类的全路径"
public class FloatingAutoHideDownBehavior extends CoordinatorLayout.Behavior<View> {
private static final Interpolator INTERPOLATOR = new DecelerateInterpolator();
private boolean mIsAnimatingOut = false; // 动画已经显示标识
private boolean mIsScrollToBottom = false;
public FloatingAutoHideDownBehavior(Context context, AttributeSet attrs) {
super();
}
/**
* onStartNestedScroll返回true时被调用
* 由上往下滑动dy<0并且滑动的力度越大dy越小;由下往上滑动dy>0并且滑动的力度越大dy越大。
*/
@Override
public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, View child, View target, int dx, int dy, int[] consumed) {
super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed);
// 手指下滑dy < 0 工具条隐藏时 translationY = child.height
if (!mIsScrollToBottom) {
float mPreTranslationY = dy + child.getTranslationY();
if (mPreTranslationY <= 0) {
child.setTranslationY(0);
mIsAnimatingOut = true;
}
if (mPreTranslationY >= child.getHeight()) {
child.setTranslationY(child.getHeight());
mIsAnimatingOut = false;
}
if (mPreTranslationY > 0 && mPreTranslationY < child.getHeight()) {
child.setTranslationY(mPreTranslationY);
mIsAnimatingOut = dy > 0;
}
}
}
/**
* 判断是否是所依赖的view。例如:return dependency instanceof ListView;
* 判断child依赖的dependency是否是ListView 只有返回true时才执行onDependentViewChanged方法
*/
@Override
public boolean layoutDependsOn(CoordinatorLayout parent, final View child, View dependency) {
if (child != null && dependency != null && dependency instanceof NestedScrollView) {
NestedScrollView s = (NestedScrollView) dependency;
if (s.getChildCount() > 0) {
// 嵌套布局底部的控件有个底部工具条控件高度的paddingBottom
View view = s.getChildAt(s.getChildCount() - 1);
view.setPadding(view.getPaddingLeft(),
view.getPaddingTop(),
view.getPaddingRight(),
view.getPaddingBottom() + child.getHeight());
}
s.setOnScrollChangeListener(new NestedScrollView.OnScrollChangeListener() {
@Override
public void onScrollChange(NestedScrollView v, int scrollX, int scrollY, int oldScrollX, int oldScrollY) {
if (v.getChildCount() > 0) {
// Grab the last child placed in the ScrollView, we need it to determinate the bottom position.
View view = v.getChildAt(v.getChildCount() - 1);
// Calculate the scrolldiff
int diff = (view.getBottom() - (v.getHeight() + scrollY));
// if diff is zero, then the bottom has been reached
Log.d("@@", "view.bottom = " + view.getBottom() + " , scrollY = " + scrollY);
if (diff == 0) { // 滑动到底部时,显示工具条
// notify that we have reached the bottom
animateIn(child);
mIsScrollToBottom = true;
} else {
mIsScrollToBottom = false;
}
}
}
});
}
return super.layoutDependsOn(parent, child, dependency);
}
/**
* 只要滑动就会被调用,注意被依赖的view需要设置setNestedScrollingEnabled(true)可以判断滑动方向,比 * 如:(nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0 判断滑动方向是否为垂直方向
*/
@Override
public boolean onStartNestedScroll(final CoordinatorLayout coordinatorLayout, final View child,
final View directTargetChild, final View target, final int nestedScrollAxes) {
// 滑动时隐藏软键盘
TDevice.hideSoftKeyboard(coordinatorLayout);
// Ensure we react to vertical scrolling 判断是否是垂直滑动方向
return nestedScrollAxes == ViewCompat.SCROLL_AXIS_VERTICAL
|| super.onStartNestedScroll(coordinatorLayout, child, directTargetChild, target, nestedScrollAxes);
}
@Override
public void onStopNestedScroll(CoordinatorLayout coordinatorLayout, View child, View target) {
super.onStopNestedScroll(coordinatorLayout, child, target);
// 完全显示或者完全隐藏 则返回
if (child.getTranslationY() == 0 || child.getTranslationY() == child.getHeight()) return;
if (mIsAnimatingOut) {
animateOut(child);
} else {
animateIn(child);
}
}
/***
* 隐藏底部工具条
* @param button
*/
private void animateOut(final View button) {
button.animate()
.translationY(button.getHeight())
.setInterpolator(INTERPOLATOR)
.setDuration(200)
.setListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
button.setTranslationY(button.getHeight());
}
});
}
/**
* 显示底部工具条
* @param button
*/
private void animateIn(final View button) {
button.animate()
.translationY(0)
.setInterpolator(INTERPOLATOR)
.setDuration(200)
.setListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
button.setTranslationY(0);
}
});
}
/**
* 点击内容栏唤起底部操作区域
* @param coordinatorLayout 外部CoordinatorLayout
* @param contentView 滚动区域
* @param bottomView 滚动时隐藏底部区域
*/
public static void showBottomLayout(CoordinatorLayout coordinatorLayout, View contentView, final View bottomView) {
bottomView.animate()
.translationY(0)
.setInterpolator(INTERPOLATOR)
.setDuration(200)
.setListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
bottomView.setTranslationY(0);
}
});
}
}
咨讯详情导航栏右上角带有数字图标的点击事件执行方法
public void scrollToComment() {
NestedScrollView nestedScrollView = mScrollView;
View target = mScrollTargetView;
if (nestedScrollView != null && target != null) {
// 默认scrollY是0, 当手指上滑的过程中scrollY变大
int curY = nestedScrollView.getScrollY();
int targetY = target.getTop();
Log.e("TAG", "curY:" + curY + " targetY:" + targetY + " mScrollYPoint:" + mScrollYPoint);
if (targetY > 0) { // 具有评论
if (curY == targetY && targetY == mScrollYPoint) {
nestedScrollView.fullScroll(View.FOCUS_UP);
} else {
if (mScrollYPoint == -1) {
nestedScrollView.smoothScrollTo(0, targetY);
mScrollYPoint = curY;
return;
}
if (curY > targetY) {
// 当前在评论之后
if (mScrollYPoint < targetY) {
nestedScrollView.smoothScrollTo(0, mScrollYPoint);
} else {
nestedScrollView.fullScroll(View.FOCUS_UP);
}
mScrollYPoint = curY;
} else {
// 当前在评论之前
nestedScrollView.smoothScrollTo(0, mScrollYPoint);
if (mScrollYPoint < curY) {
mScrollYPoint = -1;
} else {
mScrollYPoint = 0;
}
}
}
} else { // 没有评论时的逻辑
if (mScrollYPoint == -1) {
nestedScrollView.fullScroll(View.FOCUS_DOWN); // 滑动到底部,并且记录当前滑动的距离
mScrollYPoint = curY;
} else {
nestedScrollView.smoothScrollTo(0, mScrollYPoint); // 定位到先前的位置
mScrollYPoint = -1;
}
}
}
}
活动详情EventFragment.java
底部收藏工具条的收藏按钮的背景点击特效实现
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true">
<shape><solid android:color="#eeeeee"/></shape>
</item>
<item>
<shape><solid android:color="@color/white"/></shape>
</item>
</selector>
对应网页内容的显示使用了自定义的OWebView加载字符串
BlogFragment.java如下效果的实现方式,给按钮添加个背景,按钮布局是作为GridView的itemView,再将GridView作为ListView的头布局
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_activated="false">
<shape>
<corners android:radius="2dp" />
<solid android:color="@color/ques_bt_bg" />
<stroke android:width="1px" android:color="@android:color/transparent" />
</shape>
</item>
<item android:state_activated="true">
<shape>
<corners android:radius="2dp" />
<solid android:color="@color/ques_bt_bg" />
<stroke android:width="1px" android:color="@color/ques_bt_text_color_light" />
</shape>
</item>
</selector>
就拿BlogFragment.java来说综合->博客页面数据的加载过程
重写了基类onCreateView()的initWidget()在调用了super.initWidget()方法初始化了父类中ListView、SuperRefreshLayout、EmptyLayout之后就开始初始化顶部Tab标签的GridView
接着执行initData(),调用了super.initData()在父类的initData()中初始化了从子类获取到的适配器对象设置给ListView,初始化网络回调处理的对象,然后开启线程加载数据,首先读取缓存数据如果有缓存数据就显示缓存数据然后执行onRefreshing(),否则设置其他条件后开始执行onRefreshing(), 在onRefreshing()方法中先设置刷新标识位,然后调用子类重写的requestData()方法请求数据,携带父类中初始化过的网络回调处理的对象
protected void setListData(ResultBean<PageBean<T>> resultBean) {
//is refresh
mBean.setNextPageToken(resultBean.getResult().getNextPageToken());
if (mIsRefresh) { // 首次加载或者下拉刷新执行逻辑
// cache the time
mTime = resultBean.getTime();
mBean.setItems(resultBean.getResult().getItems());
mAdapter.clear();
mAdapter.addItem(mBean.getItems());
mBean.setPrevPageToken(resultBean.getResult().getPrevPageToken());
/**
* 默认加载更多由多个条件决定,mCanLoadMore默认为false,外部可控制,如果有数据则置为true,否则不具备加载更多功能
* 如果在有数据的情况下,断掉网络,上拉到底部会显示"网络错误",如果再链接网络继续上拉会更新底部布局控件显示
**/
mRefreshLayout.setCanLoadMore();
AppOperator.runOnThread(new Runnable() {
@Override
public void run() {
CacheManager.saveObject(getActivity(), mBean, CACHE_NAME);
}
});
} else { // 加载更多逻辑
mAdapter.addItem(resultBean.getResult().getItems());
}
if (resultBean.getResult().getItems().size() < 20) {
setFooterType(TYPE_NO_MORE);
// mRefreshLayout.setNoMoreData();
}
if (mAdapter.getDatas().size() > 0) {
mErrorLayout.setErrorType(EmptyLayout.HIDE_LAYOUT);
mRefreshLayout.setVisibility(View.VISIBLE);
} else {
mErrorLayout.setErrorType(EmptyLayout.NODATA);
}
}
ItemView开始的一个图标的实现
String text = "";
SpannableStringBuilder spannable = new SpannableStringBuilder(text);
if (item.isOriginal()) {
spannable.append("[icon] ");
Drawable originate = mCallback.getContext().getResources().getDrawable(R.mipmap.ic_label_originate);
if (originate != null) {
originate.setBounds(0, 0, originate.getIntrinsicWidth(), originate.getIntrinsicHeight());
}
ImageSpan imageSpan = new ImageSpan(originate, ImageSpan.ALIGN_BOTTOM);
spannable.setSpan(imageSpan, 0, 6, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
}
if (item.isRecommend()) {
spannable.append("[icon] ");
Drawable recommend = mCallback.getContext().getResources().getDrawable(R.mipmap.ic_label_recommend);
if (recommend != null) {
recommend.setBounds(0, 0, recommend.getIntrinsicWidth(), recommend.getIntrinsicHeight());
}
ImageSpan imageSpan = new ImageSpan(recommend, ImageSpan.ALIGN_BOTTOM);
if (item.isOriginal()) {
spannable.setSpan(imageSpan, 7, 13, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
} else {
spannable.setSpan(imageSpan, 0, 6, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
}
}
title.setText(spannable.append(item.getTitle()));
动弹Tab
TweetFragment.java
图片点击事件的监听抽象类,可以实现传递其他参数
private abstract class OnTweetImageClickListener implements View.OnClickListener {
@Override
public void onClick(View v) {
onClick(v, Integer.parseInt(v.getTag(R.id.iv_tweet_face).toString()),
Integer.parseInt(v.getTag(R.id.iv_tweet_image).toString()));
}
public abstract void onClick(View v, int position, int imagePosition);
}
可能有多个图片的布局容器使用的是自定义控件的FlowLayout
FlowLayout flowLayout = vh.getView(R.id.fl_image);
flowLayout.removeAllViews();
if (images != null && images.length > 0) {
flowLayout.setVisibility(View.VISIBLE);
LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams((int) Ui.dipToPx(mCallback.getContext().getResources(), 64)
, (int) Ui.dipToPx(mCallback.getContext().getResources(), 64));
for (int i = 0; i < images.length; i++) {
ImageView imageView = new ImageView(mCallback.getContext());
imageView.setLayoutParams(layoutParams);
imageView.setScaleType(ImageView.ScaleType.CENTER_CROP);
imageView.setTag(R.id.iv_tweet_image, i);
imageView.setOnClickListener(imageClickListener);
String path = images[i].getThumb();
DrawableRequestBuilder builder = getImgLoader().load(path)
.centerCrop()
.placeholder(R.color.grey_200)
.error(R.mipmap.ic_default_image_error);
if (path.toLowerCase().endsWith("gif"))
builder = builder.diskCacheStrategy(DiskCacheStrategy.SOURCE);
builder.into(imageView);
flowLayout.addView(imageView);
}
} else {
flowLayout.setVisibility(View.GONE);
}
具体细节查看TweetAdapter.java文件,图片预览细节ImageGalleryActivity.java文件
/**
* 以友好的方式显示时间
*
* @param sdate
* @return
*/
public static String friendly_time(String sdate) {
Date time = null;
if (TimeZoneUtil.isInEasternEightZones())
time = toDate(sdate);
else
time = TimeZoneUtil.transformTime(toDate(sdate),
TimeZone.getTimeZone("GMT+08"), TimeZone.getDefault());
if (time == null) {
return "Unknown";
}
String ftime = "";
Calendar cal = Calendar.getInstance();
// 判断是否是同一天
String curDate = dateFormater2.get().format(cal.getTime());
String paramDate = dateFormater2.get().format(time);
if (curDate.equals(paramDate)) {
int hour = (int) ((cal.getTimeInMillis() - time.getTime()) / 3600000);
if (hour == 0)
ftime = Math.max(
(cal.getTimeInMillis() - time.getTime()) / 60000, 1)
+ "分钟前";
else
ftime = hour + "小时前";
return ftime;
}
long lt = time.getTime() / 86400000;
long ct = cal.getTimeInMillis() / 86400000;
int days = (int) (ct - lt);
if (days == 0) {
int hour = (int) ((cal.getTimeInMillis() - time.getTime()) / 3600000);
if (hour == 0)
ftime = Math.max(
(cal.getTimeInMillis() - time.getTime()) / 60000, 1)
+ "分钟前";
else
ftime = hour + "小时前";
} else if (days == 1) {
ftime = "昨天";
} else if (days == 2) {
ftime = "前天 ";
} else if (days > 2 && days < 31) {
ftime = days + "天前";
} else if (days >= 31 && days <= 2 * 31) {
ftime = "一个月前";
} else if (days > 2 * 31 && days <= 3 * 31) {
ftime = "2个月前";
} else if (days > 3 * 31 && days <= 4 * 31) {
ftime = "3个月前";
} else {
ftime = dateFormater2.get().format(time);
}
return ftime;
}
下拉刷新上拉加载更多控件RecyclerRefreshLayout.java
TabLayout+ViewPager是一个Fragment【TweetDetailViewPagerFragment】填充了详情页的一个帧布局,ViewPager中是2个Fragment,上滑布局后TabLayot的tab会固定在顶部,同样是使用了协调布局的一个属性+自定义的一个类, 底部的发表评论布局是在KeyboardInputDelegation.java类中填充的
public class TweetDetailActivity extends BaseBackActivity implements TweetDetailContract.Operator中initWidget()方法中有如下片段,此处拿到了Framgent的属性赋值给了Activity中的属性了,当用户删除信息后需要更新Fragment中TabLayout中Tab的数据
TweetDetailViewPagerFragment mPagerFrag = TweetDetailViewPagerFragment.instantiate(this);
mCmnViewImp = mPagerFrag.getCommentViewHandler();
mThumbupViewImp = mPagerFrag.getThumbupViewHandler();
mAgencyViewImp = mPagerFrag.getAgencyViewHandler();
getSupportFragmentManager().beginTransaction()
.replace(R.id.fragment_container, mPagerFrag)
.commit();
public class TweetDetailViewPagerFragment extends Fragment implements TweetDetailContract.ICmnView, TweetDetailContract.IThumbupView, TweetDetailContract.IAgencyView {
private ViewPager mViewPager;
private TabLayout mTabLayout;
protected FragmentStatePagerAdapter mAdapter;
private TweetDetailContract.ICmnView mCmnViewImp;
private TweetDetailContract.IThumbupView mThumbupViewImp;
private TweetDetailContract.Operator mOperator;
}
mOperator的实例化
public void onAttach(Activity activity) {
super.onAttach(activity);
mOperator = (TweetDetailContract.Operator) activity;
}
mCmnViewImp
mThumbupViewImp
接口对象的实例化
onViewCreated() {
if (mAdapter == null){
final ListTweetLikeUsersFragment mCmnFrag;
mThumbupViewImp = mCmnFrag = ListTweetLikeUsersFragment.instantiate(mOperator, this);
final ListTweetCommentFragment mThumbupFrag;
mCmnViewImp = mThumbupFrag = ListTweetCommentFragment.instantiate(mOperator, this);
}
}
点赞列表页面
public class ListTweetLikeUsersFragment extends BaseRecyclerViewFragment<TweetLike> implements TweetDetailContract.IThumbupView {
private TweetDetailContract.Operator mOperator;
private TweetDetailContract.IAgencyView mAgencyView;
/** 两个接口对象的实例化 */
public static ListTweetLikeUsersFragment instantiate(TweetDetailContract.Operator operator, TweetDetailContract.IAgencyView mAgencyView) {
ListTweetLikeUsersFragment fragment = new ListTweetLikeUsersFragment();
fragment.mOperator = operator;
fragment.mAgencyView = mAgencyView;
return fragment;
}
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
mOperator = (TweetDetailContract.Operator) activity;
}
}
在initViewget()方法中
protected void initWidget(View root) {
super.initWidget(root);
mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
if (newState == RecyclerView.SCROLL_STATE_DRAGGING) {
mOperator.onScroll();
}
}
});
}
调用的就是activity中的onScroll()方法
当请求数据成功的时候机会调用mAgencyView.resetCmnCount(mAdapter.getCount());其实就是回调ViewPager所在类TweetDetailViewPagerFragment的方法更新TabLayout中对应Tab的显示