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

历经一年多努力,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点,给固定栏外层布局一个固定的高度值即可(与固定栏高度保持一致)。

另类实现方式,只用一个RecyclerView实现(传送门):Android开发之分组列表悬浮顶部栏(吸顶效果)

源码下载:

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,560评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,104评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,297评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,869评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,275评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,563评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,833评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,543评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,245评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,512评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,011评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,359评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,006评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,062评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,825评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,590评论 2 273
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,501评论 2 268

推荐阅读更多精彩内容