您的位置:首页 > 移动开发 > Android开发

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

2015-12-26 19:13 579 查看

效果图





包含的技术点

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

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


ToolBar的使用

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

[code]<?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>


此时我们的界面预览应该是这样的




接下来我们在MainActivity中进行一下设置

[code]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

[code]<?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方法,这段代码非常简单,而且我们新建项目的时候可以自动帮我们生成,我们只需要稍微修改下就可以了,我就不一一解释了

[code] @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

[code]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

[code]<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重用即可

[code]<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

[code]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添加两个方法,一个是向上滑动,一个是向下滑动

[code]public interface OnScrollListener{
        void onScroll(int scrollY);
        void onScrollToTop();
        void onScrollToBottom();
    }


那么我们怎么来判断用户是向上滑动还是向下滑动的呢?我们只需要重写ScrollView的onTouchEvent方法

[code]@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中重写这两个方法

[code] @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如下

[code]<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是

[code] showAnim = AnimationUtils.loadAnimation(getApplicationContext(),R.anim.bottom_show);
        dismissAnim = AnimationUtils.loadAnimation(getApplicationContext(),R.anim.bottom_dismiss);


[code]<?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>


[code]<?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上下载完整代码,然后再配合博客中各个功能模块的讲解,希望对大家有所帮助
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: