Android自定义控件之仿知乎详情页

效果图

包含的技术点

这个知乎的详情页面所包含的几个技术点:

1.  support.v7包下的ToolBar的使用
2.  ScrollView实现滑动顶部停靠
3.  监听手势滑动方向来显示和隐藏底部视图

ToolBar的使用

知乎的Material Design版本顶部的导航是一个ToolBar控件,ToolBar是support.v7包下的一个控件,ToolBar的使用非常简单,首先我们现在layout文件夹中新建一个ToolBar.xml

<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.Toolbar  xmlns:android="http://schemas.android.com/apk/res/android" 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" >
</android.support.v7.widget.Toolbar>

此时我们的界面预览应该是这样的
Android自定义控件之仿知乎详情页_第1张图片
接下来我们在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
Android自定义控件之仿知乎详情页_第2张图片

我们在我们的工程目录下的res文件夹上面单击右键,点击New,然后点击Image Asset
Android自定义控件之仿知乎详情页_第3张图片

在出来的菜单中的Asset Type中选择Action Bar and Tab Icons
Android自定义控件之仿知乎详情页_第4张图片

然后在Image File中选择图片的路径,在Resource name中填写图片的名称
Android自定义控件之仿知乎详情页_第5张图片

然后点击下一步就可以了,然后Android Studio就会自动帮我们创建下面这几个文件夹,并且图片也被添加进去了
Android自定义控件之仿知乎详情页_第6张图片

接下来我们在res下创建一个menu文件夹,在menu文件夹内创建一个menu_main.xml

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" >
    <item  android:id="@+id/action_share" android:title="分享" android:icon="@drawable/ic_action_share" app:showAsAction="always" />
    <item  android:id="@+id/action_menu" android:title="菜单" android:icon="@drawable/ic_action_menu" app:showAsAction="always" />

</menu>

接下来在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停靠在了顶部。
Android自定义控件之仿知乎详情页_第7张图片
我们创建两个一模一样的布局,一个是蓝色覆盖在上面,一个是红色在下面,当我们向上滑动的时候,红色的顺着滑出去,而上面的蓝色的就停留在顶部了,这样就形成了滑动顶部停靠的效果。

首先我们先自定义一个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

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" 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">

    <include  android:id="@+id/toolbar" layout="@layout/toolbar" />

    <com.zhangqi.zhihudetail.MyScrollView  android:id="@+id/myscrollview" android:layout_below="@id/toolbar" android:layout_width="match_parent" android:layout_height="wrap_content" >

        <FrameLayout  android:layout_width="match_parent" android:layout_height="match_parent" >

            <LinearLayout  android:layout_width="match_parent" android:orientation="vertical" android:layout_height="match_parent">
                <TextView  android:id="@+id/tv_title" 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" />


               <include  android:id="@+id/user_detail" layout="@layout/user_detail_view" />


                <WebView  android:id="@+id/webview" android:layout_width="match_parent" android:layout_height="wrap_content">
                </WebView>
            </LinearLayout>
            <include  android:id="@+id/top_user_detail" layout="@layout/user_detail_view" />

        </FrameLayout>
    </com.zhangqi.zhihudetail.MyScrollView>
</RelativeLayout>

其中我将顶部停靠的View的布局抽取出来了,因为要重用,所以抽取出来使用include重用即可

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/rl_user_detail"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="#ffffffff"
    android:padding="10dp">

    <ImageView
        android:id="@+id/iv_avatar"
        android:layout_width="40dp"
        android:layout_height="40dp"
        android:src="@drawable/sso_zhihu_logo" />

    <TextView
        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" />

    <TextView
        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" />

    <TextView
        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" />

    <View
        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" />
</RelativeLayout>

接下来看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如下

<LinearLayout  android:id="@+id/ll_bottom" 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">

        <TextView  android:layout_width="0dp" 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" />

        <TextView  android:layout_width="0dp" 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"/>

        <TextView  android:layout_width="0dp" 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"/>

        <TextView  android:layout_width="0dp" 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"/>
    </LinearLayout>

其中的showAnim和dismissAnim是

 showAnim = AnimationUtils.loadAnimation(getApplicationContext(),R.anim.bottom_show);
        dismissAnim = AnimationUtils.loadAnimation(getApplicationContext(),R.anim.bottom_dismiss);
<?xml version="1.0" encoding="utf-8"?>
<!--bottom_show.xml-->
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <translate  android:duration="200" android:fromYDelta="10%p" android:toYDelta="0" />
</set>
<?xml version="1.0" encoding="utf-8"?>
<!--bottom_dismiss.xml-->
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <translate  android:duration="1000" android:fromYDelta="0" android:toYDelta="100%p" />
</set>

完整代码

好了现在所有的功能都已经实现了,博客将各个功能分开写了,是为了让大家清晰了解每个功能的实现方式,但是这样确实对于项目的完整性有一定的影响,我将代码提交到了我的GitHub中,大家可以到我的GitHub上下载完整代码,然后再配合博客中各个功能模块的讲解,希望对大家有所帮助

你可能感兴趣的:(android,自定义控件,仿知乎)