AndroidAndroid 技术开发UI效果仿写

Android开发之仿微博详情页(滑动固定顶部栏效果)

2017-08-22  本文已影响4168人  李晨玮

历经一年多努力,Android 8.0 Oreo 终于和大家见面了,8月21日,全美都在追逐日全食的时候,Google在纽约宣布了Android O正式名称:奥利奥Oreo。


Android 8.0 奥利奥现已推出

哈哈,蹭个热点,也希望Android的发展越来越好,然后言归正传,先来看下我们今天要实现的效果:

滑动固定顶部栏效果图

这段时间公司准备重构一个项目,刚好用到这个效果,我就顺带写了篇文章,关于这个效果网上可以找到一些相关资料的,昨晚看了一些,感觉都不是很好,有点模棱两可的样子,也没提到需要注意的一些关键点,这里来做下整理,由于涉及到公司的代码,这里我就写个简单的Demo来讲解。

简单Demo

传统套路:

写两个一模一样的固定栏,外层用帧布局(FrameLayout)包裹,然后把外层的固定栏先隐藏,当内层的固定栏滑动到外层固定栏位置的时候,把内层固定栏隐藏,外层的固定栏显示,反之滑回来的时候把外层固定栏隐藏,内存固定栏显示。

传统套路图

这样做的有几个不好的地方:
1、重复写了一样的布局,在XML渲染的时候耗费了性能(比如更多次的测量,布局等)
2、当页面快速滚动的时候可能出现一系列的问题(布局重复,闪烁)
3、当这个固定布局带有状态的时候,逻辑会变得很复杂,比如上面那张GIF动图,固定栏中带有筛选分类,地区,年月信息,如果按照传统套路来写,那么在内层固定栏隐藏的时候需要把状态记录并且带给外层固定栏,而且相对应很多动作监听事件也需要写多次。

新套路:

这里我换了一种思路,大体布局还是不变的,只是把两个固定栏简化成了一个,只是利用removeView和addView根据坐标点在页面滑动的时候动态的把固定栏在内外部切换,这样做的好处很好的解决了上面提到的1、2点问题,当然在快速的removeView和addView还是会出现页面闪烁不自然的问题,后面会提到解决的小窍门。

先来看下XML布局:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
    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">

    <com.lcw.view.FixedHeaderScrollView.ObservableScrollView
        android:id="@+id/sv_contentView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:scrollbars="none"
        >
        <LinearLayout
            android:id="@+id/ll_contentView"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical">
        <TextView
            android:id="@+id/tv_headerView"
            android:layout_width="match_parent"
            android:layout_height="200dp"
            android:text="我是头部布局"
            android:textSize="30sp"
            android:background="#ad29e1"
            android:gravity="center"/>
            <LinearLayout
                android:id="@+id/ll_topView"
                android:layout_width="match_parent"
                android:layout_height="50dp"
                android:gravity="center"
                android:orientation="vertical">
                <TextView
                    android:id="@+id/tv_topView"
                    android:layout_width="match_parent"
                    android:layout_height="50dp"
                    android:text="我是内层固定的布局"
                    android:background="#3be42f"
                    android:textSize="30sp"
                    android:gravity="center"/>
            </LinearLayout>

            <TextView
                android:id="@+id/tv_contentView"
                android:layout_width="match_parent"
                android:layout_height="1000dp"
                android:text="我是内容布局"
                android:textSize="30sp"
                android:background="#dc7f28"
                android:paddingTop="160dp"
                android:gravity="top|center_horizontal"/>

        </LinearLayout>
        </com.lcw.view.FixedHeaderScrollView.ObservableScrollView>

    <LinearLayout
        android:id="@+id/ll_fixedView"
        android:layout_width="match_parent"
        android:layout_height="50dp"
        android:orientation="vertical"/>

</FrameLayout>

这里和上面提到的一样,最外层用了FrameLayout(RelativeLayout也可以)包裹着一个ScrollView和一个LinearLayout,当我们页面滑动到指定点的时候,需要把内层的“我是内层固定布局”移除,同时添加到外层的ViewGroup(LinearLayout)中。

自定义ScrollView,利用回调接口的方式使滑动数据对外暴露:
虽然谷歌官方给ScrollView提供了一个设置滑动监听方法setOnScrollChangeListener,不过这个方法需要基于API23之上(Android6.0系统),在日常开发中,我们需要对老系统用户进行兼容(当前兼容版本为Android4.1系统以上),所以这里我们需要去继承ScrollView并把这个监听事件通过接口的方式对外暴露,这里把这个View取名为ObservableScrollView。

package com.lcw.view.FixedHeaderScrollView;

import android.content.Context;
import android.util.AttributeSet;
import android.widget.ScrollView;

/**
 * 监听ScrollView的滑动数据
 * Create by: chenwei.li
 * Date: 2017/8/21
 * time: 11:36
 * Email: lichenwei.me@foxmail.com
 */
public class ObservableScrollView extends ScrollView{

    public ObservableScrollView(Context context) {
        this(context,null);
    }

    public ObservableScrollView(Context context, AttributeSet attrs) {
        this(context, attrs,0);
    }

    public ObservableScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }


    private OnObservableScrollViewScrollChanged mOnObservableScrollViewScrollChanged;

    public void setOnObservableScrollViewScrollChanged(OnObservableScrollViewScrollChanged mOnObservableScrollViewScrollChanged) {
        this.mOnObservableScrollViewScrollChanged = mOnObservableScrollViewScrollChanged;
    }


    public interface OnObservableScrollViewScrollChanged{
        void onObservableScrollViewScrollChanged(int l, int t, int oldl, int oldt);
    }

    /**
     * @param l Current horizontal scroll origin. 当前滑动的x轴距离
     * @param t Current vertical scroll origin. 当前滑动的y轴距离
     * @param oldl Previous horizontal scroll origin. 上一次滑动的x轴距离
     * @param oldt Previous vertical scroll origin. 上一次滑动的y轴距离
     */
    @Override
    protected void onScrollChanged(int l, int t, int oldl, int oldt) {
        super.onScrollChanged(l, t, oldl, oldt);
        if(mOnObservableScrollViewScrollChanged!=null){
            mOnObservableScrollViewScrollChanged.onObservableScrollViewScrollChanged(l,t,oldl,oldt);
        }
    }
}

这里就可以开始写我们的调用类了

package com.lcw.view.FixedHeaderScrollView;

import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.widget.LinearLayout;
import android.widget.TextView;

public class MainActivity extends AppCompatActivity implements ObservableScrollView.OnObservableScrollViewScrollChanged{

    private ObservableScrollView sv_contentView;
    private LinearLayout ll_topView;
    private TextView tv_topView;
    private LinearLayout ll_fixedView;

    //用来记录内层固定布局到屏幕顶部的距离
    private int mHeight;


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        sv_contentView= (ObservableScrollView) findViewById(R.id.sv_contentView);
        ll_topView= (LinearLayout) findViewById(R.id.ll_topView);
        tv_topView= (TextView) findViewById(R.id.tv_topView);
        ll_fixedView= (LinearLayout) findViewById(R.id.ll_fixedView);


        sv_contentView.setOnObservableScrollViewScrollChanged(this);

//        ViewTreeObserver viewTreeObserver=ll_topView.getViewTreeObserver();
//        viewTreeObserver.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
//            @Override
//            public void onGlobalLayout() {
//                ll_topView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
//                mHeight=ll_topView.getTop();
//            }
//        });

    }

    @Override
    public void onWindowFocusChanged(boolean hasFocus) {
        super.onWindowFocusChanged(hasFocus);
        if(hasFocus){
            //获取HeaderView的高度,当滑动大于等于这个高度的时候,需要把topView移除当前布局,放入到外层布局
            mHeight=ll_topView.getTop();
        }
    }

    /**
     * @param l Current horizontal scroll origin. 当前滑动的x轴距离
     * @param t Current vertical scroll origin. 当前滑动的y轴距离
     * @param oldl Previous horizontal scroll origin. 上一次滑动的x轴距离
     * @param oldt Previous vertical scroll origin. 上一次滑动的y轴距离
     */
    @Override
    public void onObservableScrollViewScrollChanged(int l, int t, int oldl, int oldt) {
            if(t>=mHeight){
                if(tv_topView.getParent()!=ll_fixedView){
                    ll_topView.removeView(tv_topView);
                    ll_fixedView.addView(tv_topView);
                }
            }else{
                if(tv_topView.getParent()!=ll_topView){
                    ll_fixedView.removeView(tv_topView);
                    ll_topView.addView(tv_topView);
                }
            }
    }
}

这里我们实现了ObservableScrollView.OnObservableScrollViewScrollChanged接口,当我们对ScrollView注册监听的时候,就可以在回调接口里拿到对应的滑动数据,其中第二个参数t就是滑动y轴的距离,现在我们只需要拿到固定布局到顶部的距离就可以判断什么时候需要移除和添加View了。

相关讲解:
1、首先我们需要知道,在Activity生命周期里的onCreate方法里对一个View去执行getWidth,getHeight,getTop,getBottom等一系列的方法是拿不到数据的,得到的结果都为0,由于此时Activity还没有得到焦点,依附在Activity的View自然也就得不到数据,所以我们需要在onResume后去进行对View的数据获取。
这里我们可以通过onGlobalLayoutListener或者onWidnowFocusChanged等方法去获取,这里的执行顺序是:Activity.onCreate->Activity.onResume->View.onMeasure->View.onLayout->onGlobalLayoutListener->Activity.onWidnowFocusChanged..(具体用哪个,看当前环境情况,比如在Fragment里是没有onWidnowFocusChanged,如果需要获取一个View的相关数据,就可以根据onGlobalLayoutListener来做,上面代码提供两种示例)

2、关于获取滑动的高度,首先我们来看一张图:


Andorid里关于View的坐标系

这里需要注意的是,除了getRawX和getRawY是相对屏幕的位置,其他的是相对应所在父布局的位置,所以在确定数据的时候,需要注意布局的嵌套。

3、当我们拿到所需要滑动的高度时,我们需要对固定布局进行临界值做判断(这里设当前滑动值为t,所需滑动值为y)
比如当我们界面一开始向上滑的时候t值是小于y值的,此时内部固定栏是不需要移除的,而当我们超过y值往回滑t值又小于y值的时候,此时内部固定栏是需要从外部移除添加到内部的,所以这里我们需要对固定栏所在的父布局(ViewGroup)做判断。

最后补充:

微博详情页

1、不管你的顶部固定栏布局多简单,建议在外套一层ViewGroup,这样方便addView的操作,不然需要去控制外层ViewGroup的addView的index位置。
2、确定View的宽高度数据可以借助onGlobalLayoutListener或者onWidnowFocusChanged来做,注意相对父布局的嵌套。
3、这种页面的设计最早来源于iOS的设计,在iOS里ScrollView嵌套TableView(相当于ListView)是没有问题的,但是在Android里,这样子的嵌套会导致ListView的复用机制作废,也就是会不断是去进行onMeasure的计算,执行多次Adapter里的getView,也就意味着多次的findViewById,使得ViewHolder失效。
4、这是个小技巧,在快速滑动的时候有些人会出现固定布局的闪烁,其实这个和removeView和addView有关系,如果你的ViewGroup设置成了warp_content,这是一个测量的耗时操作,这里只需要配合上面提到的第1点,给固定栏外层布局一个固定的高度值即可(与固定栏高度保持一致)。

源码下载:

这里附上源码地址(欢迎Star,欢迎Fork):源码下载

上一篇下一篇

猜你喜欢

热点阅读