双向滑动悬停 无需嵌套,完胜 ScrollView&ViewPager。

程序员的丰碑

介绍

PageScrollView是一个继承于ViewGroup的自定义容器类,如其名它支持ScrollViewViewPager两种滑动效果。无需嵌套LinearLayout,可支持不定宽高的子View视图。支持水平和垂直方向的布局和手势,支持任意子View滑动吸顶或是吸底悬停的交互。支持ViewPager 固有的PageTransform动画和PageChangeListener ,ScrollChangeListener等还有 View滑动时可见索引变化VisibleRangeChangeListener接口。

以下给出两张 gif 示例,演示其最基本的功能:

无需嵌套LinearLayout > scrollview.gif

ViewPager 模式 > viewpager.gif

产生的背景

项目一中需要使用ViewPager 翻页,但交互上每页有间距且要露出相邻页的部分,滑动时还要有透明度和缩放动画。首先想到用ViewPagersetPageMarginPagerAdaptergetPageWidth 返回小于 1,虽能达到效果,然无法使选中的 Item 居中在屏幕上,就果断放弃。简阅 ViewPager 源码后结合需求写了一个自定义的PageLayout 满足了需求,它就是PageScrollView 的前身。

项目二有个视图切换的 tab 要求随视图滑动到 TitleBar 下方吸顶,当时是用FrameLayout嵌套一个ScrollView和一个假的副本TabLayout 同步数据和交互,并处理滑动事件来完成的。需求完成后,就捉摸着重写一个 比ScrollView 功能更强大的滑动控件,要支持任意子视图滑动悬停。

想到刚写了个PageLayout 可改进下就能兼容ScrollView的滑动效果,读了ScrollView 源码后就开始动手写了。从此更名为PageScrollView ,真正实现了ViewPagerScrollView相应一样的交互和已知接口。改善后支持水平和垂直方向的布局和手势滑动,以及设定的任意View 悬停在边缘等 。

使用场景:

  • 完全可替代ScrollView &HorizontalScrollView 的使用场景 且少了一层LinearLayout 嵌套。
  • 方便监听滑动时子 View 可见性的变化,随时知道可见 View 的索引范围。
  • 当滑动视图内有某子View 需要随滑动吸顶或是吸底时。
  • 当滑动视图内部所有子View 都要隋滑动做Transform 动画时。
  • 当需要使用像ViewPager 或是Gallery 交互,特别适合内部子视图宽高不同或不足一屏时同时滑动选中要求居中。
  • 支持布局方向和滑动方向 动态随时切立即生效,且能恢复选中状态。
  • 可设置滑动容器的最大宽或高时,视图内容不足父容器大小时,可强制填充到父窗口大小。

如何使用

1. 在 xml 布局中使用,添加PageScrollView 标签设置可选的属性,像LinearLayout 去添加子视图的标签

<com.rexy.widget.PageScrollView
      android:id="@+id/pageScrollView"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_gravity="center"
      android:minWidth="100dp"
      android:maxWidth="400dp"
      android:minHeight="100dp"
      android:maxHeight="900dp"
      android:orientation="horizontal"
      android:gravity="center"
      rexy:childCenter="true"
      rexy:floatViewEndIndex="-1"
      rexy:floatViewStartIndex="-1"
      rexy:middleMargin="10dp"
      rexy:overFlingDistance="0dp"
      rexy:viewPagerStyle="true"
      rexy:sizeFixedPercent="0">
   <include layout="@layout/merge_childs_layout" />
</com.rexy.widget.PageScrollView>

2. (可选)在 Java 中使用,可以通过设置覆盖以上 xml 中的属性。

PageScrollView scrollView = (PageScrollView)findViewById(R.id.pageScrollView);
//设置布局方向,仅支持 水平HORIZONTAL 和 垂直VERTICAL.
scrollView.setOrientation(PageScrollView.VERTICAL);
//仅当ViewPager 模式时才能有像其一样滑动效果,OnPageChangeListener才能生效。
scrollView.setViewPagerStyle(false);
//每一个子视图按(HORIZONTAL 时)宽或(VERTICAL 时)高的百分比固定测绘。
scrollView.setSizeFixedPercent(0);
// 设置第几个视图可吸顶或吸底 取值在 [0,scrollView.getItemCount()-1]间,-1 将被忽略。
scrollView.setFloatViewStartIndex(0);
scrollView.setFloatViewEndIndex(pageScrollView.getItemCount()-1);
//强制所有子视图的 layout_gravity 属性按Gravity.CENTER 定位。
scrollView.setChildCenter(true);
//if content size less than parent size , setChildFillParent as true to match parent size.
scrollView.setChildFillParent(true);
//设置滚动方向的子视图间距。
scrollView.setMiddleMargin(30);
//设置容器本身在测绘时的最大宽和高。
scrollview.setMaxWidth(400);
scrollview.setMaxHeigh(800);

3. (可选)绑定事件,实现接口。

//接着上面
scrollView.setPageHeadView(headerView); //设置头部 View
scrollView.setPageFooterView(footerView); 设置尾部 View
//设置 PageTransformer 动画,实现滑动视图的变换。
scrollView.setPageTransformer(new PageScrollView.PageTransformer() {
@Override
public void transformPage(View view, float position, boolean horizontal) {
//在这里根据滑动相对偏移量 position,实现该视图的动画效果。
}
@Override
public void recoverTransformPage(View view, boolean horizontal) {
//清除视图的动画效果,在setPageTransformer(null)时会调用。
}
});
PageScrollView.OnPageChangeListener pagerScrollListener = new PageScrollView.OnPageChangeListener() {
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
// ViewPager 滑动视图时,相对偏移适时回调。
}
@Override
public void onPageSelected(int position, int oldPosition) {
// ViewPager 模式时 选中回调。
}
@Override
public void onScrollChanged(int scrollX, int scrollY, int oldScrollX, int oldScrollY) {
//视图滑动回调 View.onScrollChanged
}
@Override
public void onScrollStateChanged(int state, int oldState) {
//state 的取值如下,标明着容器的滑动状态。
// SCROLL_STATE_IDLE = 0; // 滑动停止状态。
// SCROLL_STATE_DRAGGING = 1;//用户正开始拖拽滑动 。
// SCROLL_STATE_SETTLING = 2;//开始松开手指快速滑动。
}
};
scrollView.setOnPageChangeListener(pagerScrollListener);
// 设置视图滚动的监听。
scrollView.setOnScrollChangeListener(pagerScrollListener);
//设置可见子 View 发生变化时 可见索引区间的监听。
scrollView.setOnVisibleRangeChangeListener(new OnVisibleRangeChangeListener(){
    public void onVisibleRangeChanged(int firstVisible, int lastVisible, int oldFirstVisible, int oldLastVisible){
    }
});

实现简介

实现一个基本的容器控件需继承于 ViewGroup 写重写其onMeasureonLayout 分别实现控件本身大小的测量和子View的布局定位。若需手势交互还得处理onInterceptTouchEventonTouchEvent事件。下面仅以垂直方向布局来说明 PageScrollView的实现步骤(水平方向同理)以此讲解如何自定义一个最简单的 ViewGroup

1.onMeasure测量内容和自身大小,终需调setMeasuredDimension

begin: contentWidth=0,contentHeight=0;
for child(only not GONE) in all views do
child.measure(childMeasureSpecWidth,childMeasureHeight);
contentWidth=Math.max(contentWidth,child.getMeasureWidth());//(暂忽略 layoutMargin ,下同)
contentHeight+=child.getMeasureHeight(); //处理滑动,这个 contentHeight 是要存起来的.
done
measureWidth=resolveSize(contentWidth,widthMeasureSpec);//暂忽padding ,minimumWidth 下同
measureHeight=resolveSize(contentHeight,heightMeasureSpec);
setMeasuredDimension(measureWidth,measureHeight);//此方法调用后自身大小就定了。
end

2. onLayout定位所有子View 在自身窗口上的位置,调用 child.layout

begin: childTop=getPaddingTop(),baseLeft=getPaddingLeft();
for child(only not GONE) in all views do //忽LayoutParams
childLeft=baseLeft,childRight=childLeft+child.getMeasureWidth();
childBottom=childTop+child.getMeasureHeight();
child.layout(childLeft,childTop,childRight,childBottom);
childTop=childBottom;
done
end
至此所有的子 View 就能在垂直方向排列显示出来了。

3. 计算滑动区间&编写滑动方法

重写computeVerticalScrollRange 同方向相关有三个方法(Offset/Extra)。
据第一步就得到了 contentHeight,再根据自身高度 getHeight()就可等到滑动区间了。
scrollRange=contentHeight-getHeight();暂时忽略 容器的padding.
View 本身有 scrollToscrollBy 来滑动自身内容。只需计算滑动偏移量,规整到[0,scrollRange] 间。然后直接应用 scrollTo or scrollByinvalidate
若要平滑动画就需要 Scroller 类 startScroll,并处理computeScroll() 如下:

@Override public void computeScroll() {
    if (!mScroller.isFinished() && mScroller.computeScrollOffset()) {
        int oldX = getScrollX();
        int oldY = getScrollY();
        int x = mScroller.getCurrX();
        int y = mScroller.getCurrY();
        if (oldX != x || oldY != y) {
            scrollTo(x, y);
        }
        ViewCompat.postInvalidateOnAnimation(this);
    } else {
        if (mScrollState == ViewPager.SCROLL_STATE_SETTLING) {
            post(mIdleExecute);
        }
    }
}

4. 处理触屏事件 关注垂直方向 Touch 事件达到交互上的滑动。

onInterceptTouchEventonTouchEvent两个方法中都要处理ACTION_MOVE 时判断垂直方向的绝对滑动值是否大于临界值 且大于水平滑动绝对值 来标志当前正准备滑动 isBingDragged=true ;让 onInterceptTouchEvent 返回值为isBeingDragged 当然ACTION_DOWN 时需要返回false .
onInterceptTouchEvent返回true 表示拦截了事件,将会走自身的onTouchEvent 让它返回true,所以后面所有要处理滑动的逻辑只需要在onTouchEvent里处理即可。
根据每次ACTION_MOVE滑动的 dy 来计算内容视图需要滑动到的newScrollY. scrollTo(0,newScrollY) .
ACTION_CANCEL&ACTION_UP 时,计算滑动速度(VelocityTracker)并根据滑动方向来 处理自动滑动交互 mScroller.startScroll 。此时的滑动目标距离和动画时间可借鉴ScrollView和ViewPager的源码逻辑。

总结

以上实现部分讲的是最基本的原理,功能和支持的属性越多实际考虑的细节和实现就会越复杂。
示例工程在 github上持续更新,可直接搜索** PageScrollView**,有兴趣同学可查看源码

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

推荐阅读更多精彩内容