效果图
包含的技术点
这个知乎的详情页面所包含的几个技术点:
1. support.v7包下的ToolBar的使用
2. ScrollView实现滑动顶部停靠
3. 监听手势滑动方向来显示和隐藏底部视图
ToolBar的使用
知乎的Material Design版本顶部的导航是一个ToolBar控件,ToolBar是support.v7包下的一个控件,ToolBar的使用非常简单,首先我们现在layout文件夹中新建一个ToolBar.xml
xmlns:local="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/toolbar"
android:background="?attr/colorPrimary"
local:theme="@style/Base.ThemeOverlay.AppCompat.Dark.ActionBar"
android:popupTheme="@style/ThemeOverlay.AppCompat.Light"
>
此时我们的界面预览应该是这样的
接下来我们在MainActivity中进行一下设置
public class MainActivity extends AppCompatActivity{
private Toolbar mToolbar;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mToolbar = (Toolbar) findViewById(R.id.toolbar);
mToolbar.setTitle("历史上有哪些打脸的故事?");
setSupportActionBar(mToolbar);
}
接下来我们添加ToolBar中的分享和菜单按钮功能,他们是menu
我们在我们的工程目录下的res文件夹上面单击右键,点击New,然后点击Image Asset
在出来的菜单中的Asset Type中选择Action Bar and Tab Icons
然后在Image File中选择图片的路径,在Resource name中填写图片的名称
然后点击下一步就可以了,然后Android Studio就会自动帮我们创建下面这几个文件夹,并且图片也被添加进去了
接下来我们在res下创建一个menu文件夹,在menu文件夹内创建一个menu_main.xml
xmlns:app="http://schemas.android.com/apk/res-auto"
>
android:title="分享"
android:icon="@drawable/ic_action_share"
app:showAsAction="always"
/>
android:title="菜单"
android:icon="@drawable/ic_action_menu"
app:showAsAction="always"
/>
接下来在MainActivity中重写onCreateOptionsMenu和onOptionsItemSelected方法,这段代码非常简单,而且我们新建项目的时候可以自动帮我们生成,我们只需要稍微修改下就可以了,我就不一一解释了
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.menu_main, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
int id = item.getItemId();
if (id == R.id.action_share){
Toast.makeText(MainActivity.this,"分享",Toast.LENGTH_SHORT).show();
return true;
}
if (id == R.id.action_menu){
Toast.makeText(MainActivity.this,"菜单",Toast.LENGTH_SHORT).show();
return true;
}
return super.onOptionsItemSelected(item);
}
好了,ToolBar已经准备完毕了,我们可以运行一下项目看一下效果,是不是和知乎的一样!
ScrollView实现滑动顶部停靠
这个功能我是参考Android 仿美团网,大众点评购买框悬浮效果之修改版这篇文章来实现的,他的实现思路非常牛x,我最开始的想法是监听要停留在顶部的View的滑动位置,当它滑动到顶部的时候再创建出一个和他一模一样的View显示在顶部的位置,但是我发现这样做非常的麻烦,但是这篇文章的作者使用了另一种思路,他将两个View都创建出来,只不过他们一开始是重合的,我们看上去就像只有一个View一样,当View滑动到顶部时,上面覆盖的那个View就固定住了,从而我们视觉上感觉是这个View停靠在了顶部。
我们创建两个一模一样的布局,一个是蓝色覆盖在上面,一个是红色在下面,当我们向上滑动的时候,红色的顺着滑出去,而上面的蓝色的就停留在顶部了,这样就形成了滑动顶部停靠的效果。
首先我们先自定义一个ScrollView,由于ScrollView没有onScrollListener,所以我们必须要自己写一个onScrollListener
public class MyScrollView extends ScrollView{
private OnScrollListener mListener;
public interface OnScrollListener{
void onScroll(int scrollY);
}
public MyScrollView(Context context) {
super(context);
}
public MyScrollView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public MyScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public void setOnScrollListener(OnScrollListener listener){
mListener = listener;
}
@Override
protected int computeVerticalScrollRange() {
return super.computeVerticalScrollRange();
}
@Override
protected void onScrollChanged(int l, int t, int oldl, int oldt) {
super.onScrollChanged(l, t, oldl, oldt);
if (mListener!=null){
mListener.onScroll(t);
}
}
}
我们在onScrollChanged中可以获得当前ScrollView的滑动位置,我们回调调用mListener的onScroll方法并且将当前ScrollView的滑动位置传给MainActivity
在MainActivity中我们实现MyScrollView.OnScrollListener接口,并且重写onScroll方法,在onScroll方法中设置蓝色View的位置为和红色View重合
先来看一下activity_main.xml
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:id="@+id/container"
tools:context="com.zhangqi.zhihudetail.MainActivity">
layout="@layout/toolbar" />
android:layout_below="@id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
>
android:layout_height="match_parent"
>
android:orientation="vertical"
android:layout_height="match_parent">
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#efefef"
android:paddingBottom="10dp"
android:paddingLeft="5dp"
android:paddingTop="10dp"
android:text="历史上有哪些打脸的故事?"
android:textColor="#ababab"
android:textSize="18sp" />
layout="@layout/user_detail_view"
/>
android:layout_width="match_parent"
android:layout_height="wrap_content">
layout="@layout/user_detail_view"
/>
其中我将顶部停靠的View的布局抽取出来了,因为要重用,所以抽取出来使用include重用即可
android:id="@+id/rl_user_detail"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#ffffffff"
android:padding="10dp">
android:id="@+id/iv_avatar"
android:layout_width="40dp"
android:layout_height="40dp"
android:src="@drawable/sso_zhihu_logo" />
android:id="@+id/tv_nickname"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toRightOf="@id/iv_avatar"
android:paddingLeft="5dp"
android:text="神灯"
android:textColor="#ff000000"
android:textSize="16sp" />
android:id="@+id/tv_detail"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignLeft="@id/tv_nickname"
android:layout_below="@id/tv_nickname"
android:paddingLeft="5dp"
android:paddingTop="5dp"
android:text="阿拉灯神灯"
android:textColor="#bcbcbc" />
android:id="@+id/tv_like_num"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
android:drawableLeft="@drawable/ic_vote_normal"
android:drawablePadding="10dp"
android:gravity="center_vertical"
android:padding="5dp"
android:text="3028" />
android:layout_width="1dp"
android:layout_height="wrap_content"
android:layout_alignBottom="@id/tv_like_num"
android:layout_alignTop="@id/tv_like_num"
android:layout_centerVertical="true"
android:layout_marginBottom="2dp"
android:layout_marginRight="5dp"
android:layout_marginTop="2dp"
android:layout_toLeftOf="@id/tv_like_num"
android:background="#CCC" />
接下来看MainActivity
public class MainActivity extends AppCompatActivity implements MyScrollView.OnScrollListener{
//自定义的ScrollView
private MyScrollView mScrollView;
//随着ScrollView滑走的View
private RelativeLayout mUserDetail;
//固定在顶部的View
private RelativeLayout mTopUserDetail;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initView();
}
private void initView() {
mUserDetail = (RelativeLayout) findViewById(R.id.user_detail);
mTopUserDetail = (RelativeLayout) findViewById(R.id.top_user_detail);
mScrollView = (MyScrollView) findViewById(R.id.myscrollview);
mScrollView.setOnScrollListener(this);
//当布局中所有的View都测量完后回回调的方法,我们在这个方法中可以拿到View的宽和高
//在这个方法中调用onScroll是为什么?
//因为我们要在onScroll中获得mUserDetail距顶部的高度
//只有在所有的View都测量完后我们才能拿到这个高度值,否则我们拿到的是0
//所以在onGlobalLayout中调用一下onScroll方法,我们一定可以拿到mUserDetail这个View
//距离屏幕顶部的距离,从而设置给我们的mTopUserDetail这个View,实现两个View的重合
findViewById(R.id.container).getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
onScroll(mScrollView.getScrollY());
}
});
}
@Override
public void onScroll(int scrollY) {
//在最开始mUserDetail距离屏幕顶部是有一段距离的,而最开始scrollY=0,
//所以在最开始的时候我们取两者的最大值就可以使两个View重合起来
//因为我们是在所有的View都测量完毕后调用过onScroll方法的,
//所以mUserDetail.getTop()得到的值是正确的值
int userDetailView2Top = Math.max(scrollY, mUserDetail.getTop());
//调用mTopUserDetail的layout方法,设置其在屏幕上的位置
mTopUserDetail.layout(0, userDetailView2Top, mTopUserDetail.getWidth(), userDetailView2Top + mTopUserDetail.getHeight());
}
}
现在我们已经可以实现滑动停靠的功能了,接下来我们再来实现屏幕底部的View随着滑动方向显示和隐藏的功能
屏幕底部View随滑动方向显示和隐藏功能
我们看到屏幕底部有一个布局,当我们手指向上滑动的时候,底部的View是隐藏的,为了给我们更好地阅读体验,当我们手指向下滑动的时候,底部的View是显示出来的,提供给我们一些功能。
那么我们就要修改刚才自定义的ScrollView,给onScrollListener添加两个方法,一个是向上滑动,一个是向下滑动
public interface OnScrollListener{
void onScroll(int scrollY);
void onScrollToTop();
void onScrollToBottom();
}
那么我们怎么来判断用户是向上滑动还是向下滑动的呢?我们只需要重写ScrollView的onTouchEvent方法
@Override
public boolean onTouchEvent(MotionEvent ev) {
if (mListener!=null) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
//记录按下时的Y坐标
downY = (int) ev.getY();
break;
case MotionEvent.ACTION_MOVE:
//记录滑动时的Y坐标
int moveY = (int) ev.getY();
//计算出一个差值
offsetY = moveY - downY;
downY = moveY;
break;
case MotionEvent.ACTION_UP:
//当手指抬起时判断差值的大小
if (offsetY < 0) {//如果小于0,则说明用户手指向上滑动
mListener.onScrollToBottom();
}else{//如果大于0,则说明用户手指向下滑动
mListener.onScrollToTop();
}
break;
}
}
return super.onTouchEvent(ev);
}
接下来我们要在MainActivity中重写这两个方法
@Override
public void onScrollToTop() {
if (!ll_bottom.isShown()) {
ll_bottom.clearAnimation();
ll_bottom.startAnimation(showAnim);
ll_bottom.setVisibility(View.VISIBLE);
}
}
@Override
public void onScrollToBottom() {
if (ll_bottom.isShown()) {
ll_bottom.clearAnimation();
ll_bottom.startAnimation(dismissAnim);
ll_bottom.setVisibility(View.GONE);
}
}
其中ll_bottom就是我们底部的布局,他的xml如下
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="10dp"
android:paddingBottom="10dp"
android:layout_alignParentBottom="true"
android:background="#ffffffff"
android:orientation="horizontal">
android:layout_height="wrap_content"
android:layout_weight="1"
android:drawableTop="@drawable/ic_nohelp"
android:text="没有帮助"
android:textColor="#ababab"
android:drawablePadding="10dp"
android:gravity="center_horizontal"
/>
android:layout_height="wrap_content"
android:layout_weight="1"
android:drawableTop="@drawable/ic_thank"
android:text="感谢"
android:textColor="#ababab"
android:drawablePadding="10dp"
android:gravity="center_horizontal"/>
android:layout_height="wrap_content"
android:layout_weight="1"
android:drawableTop="@drawable/ic_collect"
android:text="收藏"
android:textColor="#ababab"
android:drawablePadding="10dp"
android:gravity="center_horizontal"/>
android:layout_height="wrap_content"
android:layout_weight="1"
android:drawableTop="@drawable/ic_comment"
android:text="评论 184"
android:textColor="#ababab"
android:drawablePadding="10dp"
android:gravity="center_horizontal"/>
其中的showAnim和dismissAnim是
showAnim = AnimationUtils.loadAnimation(getApplicationContext(),R.anim.bottom_show);
dismissAnim = AnimationUtils.loadAnimation(getApplicationContext(),R.anim.bottom_dismiss);
android:fromYDelta="10%p"
android:toYDelta="0"
/>
android:fromYDelta="0"
android:toYDelta="100%p"
/>
完整代码
好了现在所有的功能都已经实现了,博客将各个功能分开写了,是为了让大家清晰了解每个功能的实现方式,但是这样确实对于项目的完整性有一定的影响,我将代码提交到了我的GitHub中,大家可以到我的GitHub上下载完整代码,然后再配合博客中各个功能模块的讲解,希望对大家有所帮助