热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

Android打造顶部停留控件,可用于所有可滚动的控件(ScrollView,ListView)

1、序言现在很多App为了让一个页面可以有更多展示的东西。于是乎有一个界面就有几个tab进行切换页面,同时滚动的时候为了方便用户切换tab,这时tab需要悬浮在布局的顶部。所以这样就有了这

1、序言

现在很多App为了让一个页面可以有更多展示的东西。于是乎有一个界面就有几个tab进行切换页面,同时滚动的时候为了方便用户切换tab,这时tab需要悬浮在布局的顶部。所以这样就有了这篇blog咯…….

2、实现原理

控件的实现原理,相对来还是比较简单的:
1、首先自定义一个GroupView,实现滑动的效果,同时进行一些判断,比如:当满足一些条件时,把事件处理交给ChildView来处理;当ChildView满足一些条件时(比如ListView滚动到了第一条数据,ScrollView滚动到了顶部),让GroupView滚动,ChildView停止滚动。
2、然后自定义一个ChildView,这个可以是ListView、ScrollView等等可滚动的控件,重写onTouchEvent方法,进行判断查看是否可以滚动,因为是否可以滚动是由GroupView来控制的。
3、通过接口的方式把两者之间判断是否可以滚动联系起来。

3、实现代码

看逻辑不清楚可以跳过直接看代码:
首先是GroupView的代码:

public class SideGroupLayout extends ViewGroup {
    public static final String TAG = "android_xw";

    private int mTouchSlop;
    private float mLastMotionX;
    private float mLastMotionY;
    private boolean mIsBeingDragged;
    protected int mFirstItemHeight;
    private int mScrollY;
    public boolean mScrollToEnd;

    private VelocityTracker mVelocityTracker;
    private int mMinimumFlingVelocity;
    private int mMaximumFlingVelocity;

    private Scroller mScroller;
    private boolean mCanScroller;

    public SideGroupLayout(Context context, AttributeSet attrs) {
        super(context, attrs);

        mCanScroller = true;
        ViewConfiguration cOnfiguration= ViewConfiguration.get(context);
        mTouchSlop = configuration.getScaledTouchSlop();

        mMinimumFlingVelocity = configuration.getScaledMinimumFlingVelocity();
        mMaximumFlingVelocity = configuration.getScaledMaximumFlingVelocity();

        mScroller = new Scroller(context);
        reset();
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int width = MeasureSpec.getSize(widthMeasureSpec);
        int height = 0;

        for (int i = 0; i if (child.getVisibility() != View.GONE) {
                measureChild(child, widthMeasureSpec, heightMeasureSpec);
                height += child.getMeasuredHeight();
            }
        }
        setMeasuredDimension(width, height);
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        int height = 0;
        mFirstItemHeight = 0;
        for (int i = 0; i if (view.getVisibility() != View.GONE) {
                view.layout(0, height, getWidth(), height + view.getMeasuredHeight());
                height += view.getMeasuredHeight();
                if (i == 0) {
                    mFirstItemHeight = height;
                }
            }
        }
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        if (mFirstItemHeight == 0 || !mCanScroller) {
            mScrollToEnd = true;
            return super.onInterceptTouchEvent(ev);
        }

        final int action = ev.getAction();
        if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) {
            return true;
        }
        switch (action & MotionEvent.ACTION_MASK) {
        case MotionEvent.ACTION_MOVE: {
            final float x = ev.getX();
            final float y = ev.getY();
            final int xDiff = (int) Math.abs(x - mLastMotionX);
            final int yDiff = (int) Math.abs(y - mLastMotionY);
            // Log.i("TAG", "mScrollY == mFirstItemHeight:" + (mScrollY ==
            // mFirstItemHeight));
            if (mScrollY == mFirstItemHeight) {
                boolean isScrollY = yDiff > xDiff && y > mLastMotionY && mAction != null && mAction.isGroupScroll();
                return isScrollY;
            } else if (yDiff > mTouchSlop * 2 && yDiff >= xDiff) {
                mIsBeingDragged = true;
                mLastMotiOnY= y;
            }
            break;
        }
        case MotionEvent.ACTION_DOWN: {
            mLastMotiOnX= ev.getX();
            mLastMotiOnY= ev.getY();
            mIsBeingDragged = false;
            break;
        }
        case MotionEvent.ACTION_CANCEL:
        case MotionEvent.ACTION_UP:
            mIsBeingDragged = false;
            break;
        }
        return mIsBeingDragged;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (mFirstItemHeight == 0 || !mCanScroller) {
            mScrollToEnd = true;
            return super.onTouchEvent(event);
        }
        addVelocityTracker(event);

        final int action = event.getAction();
        final float y = event.getY();
        final float x = event.getX();

        switch (action) {
        case MotionEvent.ACTION_DOWN:
            // 获取相对屏幕的坐标,即以屏幕左上角为原点
            break;
        case MotionEvent.ACTION_MOVE:
            final float scrollX = mLastMotionX - x;
            final float scrollY = mLastMotionY - y;
            onScroll((int) scrollX, (int) scrollY);
            scrollTo(0, mScrollY);
            mLastMotiOnX= x;
            mLastMotiOnY= y;
            break;
        case MotionEvent.ACTION_UP:
        case MotionEvent.ACTION_CANCEL:
            mVelocityTracker.computeCurrentVelocity(1000, mMaximumFlingVelocity);
            final float velocityX = mVelocityTracker.getXVelocity();
            final float velocityY = mVelocityTracker.getYVelocity();
            if (Math.abs(velocityY) > mMinimumFlingVelocity * 3 && Math.abs(velocityY) > Math.abs(velocityX)) {
                onFling(velocityX, velocityY);
            }
            cancel();
            break;
        }
        return true;
    }

    private void onScroll(int scrollX, int scrollY) {
        if (scrollY > 0) {
            if (mScrollY == mFirstItemHeight)
                return;
            if (mScrollY + scrollY >= mFirstItemHeight) {
                mScrollY = mFirstItemHeight;
            } else {
                mScrollY = mScrollY + scrollY;
            }
        } else if (scrollY <0) {
            if (mScrollY > 0) {
                scrollY = Math.abs(scrollY);
                if (mScrollY - scrollY <= 0) {
                    mScrollY = 0;
                } else {
                    mScrollY = mScrollY - scrollY;
                }
            }
        }
        mScrollToEnd = mScrollY == mFirstItemHeight;
    }

    private void onFling(float velocityX, float velocityY) {
        int dy = 0;
        if (velocityY > 0) {
            dy = -mScrollY;
        } else {
            dy = (int) (mFirstItemHeight - getScrollY());
        }

        float ratio = getRatio(Math.abs(velocityY));
        dy = (int) (dy * ratio);
        onScroll(0, dy);
        if (mFirstItemHeight > 0) {
            mScroller.startScroll(0, getScrollY(), 0, dy, 500 * Math.abs(dy) / mFirstItemHeight);
        }
        postInvalidate();
    }

    protected float getRatio(float velocityY) {
        return 1;
    }

    @Override
    public void computeScroll() {
        super.computeScroll();
        if (mScroller.computeScrollOffset()) {
            scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
            onScrollChanged(getScrollX(), getScrollY(), 0, 0);
            postInvalidate();
        }
    }

    private void addVelocityTracker(MotionEvent event) {
        if (mVelocityTracker == null)
            mVelocityTracker = VelocityTracker.obtain();
        mVelocityTracker.addMovement(event);
    }

    private void cancel() {
        if (mVelocityTracker != null) {
            mVelocityTracker.recycle();
            mVelocityTracker = null;
        }
        mIsBeingDragged = false;
    }

    private void reset() {
        mScrollToEnd = false;
    }

    public boolean isScrollToEnd() {
        return mScrollToEnd;
    }

    private OnGroupScrollListener mAction;

    public void setOnGroupScrollListener(OnGroupScrollListener action) {
        this.mAction = action;
    }


    public void setCanScroller(boolean canScroller) {
        this.mCanScroller = canScroller;
    }

    @Override
    protected void onScrollChanged(int l, int t, int oldl, int oldt) {
        super.onScrollChanged(l, t, oldl, oldt);
        if (mAction != null) {
            mAction.onScrollChanged(l, t);
        }
    }

    public void onActivityDestory() {
        reset();
        mScroller = null;
        mScrollToEnd = false;
    }

}

代码还是不复杂的,玩过自定义控件都知道怎么回事,根据这个需求重点说下几个方法:

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        int height = 0;
        mFirstItemHeight = 0;
        for (int i = 0; i if (view.getVisibility() != View.GONE) {
                view.layout(0, height, getWidth(), height + view.getMeasuredHeight());
                height += view.getMeasuredHeight();
                if (i == 0) {
                    mFirstItemHeight = height;
                }
            }
        }
    }



    private void onScroll(int scrollX, int scrollY) {
        if (scrollY > 0) {
            if (mScrollY == mFirstItemHeight)
                return;
            if (mScrollY + scrollY >= mFirstItemHeight) {
                mScrollY = mFirstItemHeight;
            } else {
                mScrollY = mScrollY + scrollY;
            }
        } else if (scrollY <0) {
            if (mScrollY > 0) {
                scrollY = Math.abs(scrollY);
                if (mScrollY - scrollY <= 0) {
                    mScrollY = 0;
                } else {
                    mScrollY = mScrollY - scrollY;
                }
            }
        }
        mScrollToEnd = mScrollY == mFirstItemHeight;
    }


重点看下if(i == 0)时会执行的代码,mFirstItemHeight 这是获取第一个ChildView的高度,然后可以在onScroll()方法里面一个赋值:mScrollToEnd = mScrollY == mFirstItemHeight; ChildView就是通过这个参数mScrollToEnd,来判断是否要进行滚动。可以看到当我们滚动的Y轴的距离等于第一控件的高度,这时会把mScrollToEnd复制为true,这个时候事件就会被ChildView给消化掉,这个时候滚动的时候,就是滚动ChildView了。

再看下事件拦截方法里面:


    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        if (mFirstItemHeight == 0 || !mCanScroller) {
            mScrollToEnd = true;
            return super.onInterceptTouchEvent(ev);
        }

        final int action = ev.getAction();
        if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) {
            return true;
        }
        switch (action & MotionEvent.ACTION_MASK) {
        case MotionEvent.ACTION_MOVE: {
            final float x = ev.getX();
            final float y = ev.getY();
            final int xDiff = (int) Math.abs(x - mLastMotionX);
            final int yDiff = (int) Math.abs(y - mLastMotionY);
            // Log.i("TAG", "mScrollY == mFirstItemHeight:" + (mScrollY ==
            // mFirstItemHeight));
            if (mScrollY == mFirstItemHeight) {
                boolean isScrollY = yDiff > xDiff && y > mLastMotionY && mAction != null && mAction.isGroupScroll();
                return isScrollY;
            } else if (yDiff > mTouchSlop * 2 && yDiff >= xDiff) {
                mIsBeingDragged = true;
                mLastMotiOnY= y;
            }
            break;
        }
        case MotionEvent.ACTION_DOWN: {
            mLastMotiOnX= ev.getX();
            mLastMotiOnY= ev.getY();
            mIsBeingDragged = false;
            break;
        }
        case MotionEvent.ACTION_CANCEL:
        case MotionEvent.ACTION_UP:
            mIsBeingDragged = false;
            break;
        }
        return mIsBeingDragged;
    }

我们主要看这句代码:

if (mScrollY == mFirstItemHeight) {
                boolean isScrollY = yDiff > xDiff && y > mLastMotionY && mAction != null && mAction.isGroupScroll();
                return isScrollY;

当父控件滑动的距离等于第一个ChildView高度时,会做一个判断:当是向上滑动,并且滚动的ChidlView让GroupView滚动时,会把事件拦截下来,交给GroupView来进行处理,所以这时就是GroupView进行滚动,而滚动的ChildView就会停止滚动。

GroupView其他的代码稍作讲解:

onMeasure()里面对所有ChildView进行一个高度的计算,然后才能得知GroupView的高度; onLayout()里面对ChildView进行位置的确认; onInterceptTouchEvent()已经说过了,跳过; onTouchEvent()是进行事件处理,因为是集成的GroupView,不能自己滚动,所以我们要利用Scroller来实现一个类似于ScrollView滚动的效果,写过这种控件相信都明白的。 其他的一些方法都是为实现滚动而写的一些方法。

GroupView的实现比较复杂一些,相对来说ChildView的实现就非常简单了:
来看一个可以嵌套这个SideGroupLayout的ScrollView:


public class SideTopScrollView extends ScrollView {

    private OnChildScrollListener mAction;

    private boolean isScrollTop = false;

    public SideTopScrollView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        boolean bool = mAction != null && mAction.isChildScroll() && super.onInterceptTouchEvent(ev);
        return bool;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        return mAction != null && mAction.isChildScroll() && super.onTouchEvent(event);
    }

    @Override
    protected void onScrollChanged(int l, int t, int oldl, int oldt) {
        if (t == 0) {
            isScrollTop = true;
        } else {
            isScrollTop = false;
        }
        super.onScrollChanged(l, t, oldl, oldt);
    }

    public boolean isScrollToTop() {
        return isScrollTop;
    }

    public void setOnChildScrollListener(OnChildScrollListener action) {
        this.mAction = action;
    }


}

可以看到逻辑是非常简单的:
1、重写onTouchEvent()方法,问一下SideGroupLayout,我是不是可以滚动了。
2、重写onScrollChanged()方法,告诉SideGroupLayout,你是不是可以滚动了。

主要的代码都在上面,下面我们看使用方式:

public class ScrollViewActivity extends Activity implements OnGroupScrollListener, OnChildScrollListener {

    private SideGroupLayout mHoverLayout;
    private SideTopScrollView mSideTopScrollView;

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

    private void initView() {
        mHoverLayout = (SideGroupLayout) findViewById(R.id.hoverlayout);
        mSideTopScrollView = (SideTopScrollView) findViewById(R.id.sidescrollview);
        mHoverLayout.setOnGroupScrollListener(this);
        mSideTopScrollView.setOnChildScrollListener(this);
    }

    @Override
    public boolean isChildScroll() {
        return mHoverLayout != null && mHoverLayout.isScrollToEnd();
    }

    @Override
    public boolean isGroupScroll() {
        return mSideTopScrollView != null && mSideTopScrollView.isScrollToTop();
    }

    @Override
    public void onScrollChanged(int left, int top) {
    }

}

layout_scrollview:


<widget.SideGroupLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/hoverlayout" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" >

    <TextView  android:layout_width="match_parent" android:layout_height="250dp" android:background="@android:color/darker_gray" android:gravity="center" android:text="可滚动的区域" />

    <TextView  android:layout_width="match_parent" android:layout_height="50dp" android:background="@android:color/black" android:gravity="center" android:text="停留的位置" android:textColor="@android:color/white" />

    <widget.SideTopScrollView  android:id="@+id/sidescrollview" android:layout_width="match_parent" android:layout_height="match_parent" >

        <LinearLayout  android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" >

            <TextView  android:layout_width="match_parent" android:layout_height="50dp" android:layout_gravity="center" android:text="内容" />

            <TextView  android:layout_width="match_parent" android:layout_height="50dp" android:layout_gravity="center" android:text="内容" />

            <TextView  android:layout_width="match_parent" android:layout_height="50dp" android:layout_gravity="center" android:text="内容" />

            <TextView  android:layout_width="match_parent" android:layout_height="50dp" android:layout_gravity="center" android:text="内容" />

            <TextView  android:layout_width="match_parent" android:layout_height="50dp" android:layout_gravity="center" android:text="内容" />

            <TextView  android:layout_width="match_parent" android:layout_height="50dp" android:layout_gravity="center" android:text="内容" />

            <TextView  android:layout_width="match_parent" android:layout_height="50dp" android:layout_gravity="center" android:text="内容" />

            <TextView  android:layout_width="match_parent" android:layout_height="50dp" android:layout_gravity="center" android:text="内容" />

            <TextView  android:layout_width="match_parent" android:layout_height="50dp" android:layout_gravity="center" android:text="内容" />

            <TextView  android:layout_width="match_parent" android:layout_height="50dp" android:layout_gravity="center" android:text="内容" />

            <TextView  android:layout_width="match_parent" android:layout_height="50dp" android:layout_gravity="center" android:text="内容" />

            <TextView  android:layout_width="match_parent" android:layout_height="50dp" android:layout_gravity="center" android:text="内容" />

            <TextView  android:layout_width="match_parent" android:layout_height="50dp" android:layout_gravity="center" android:text="内容" />

            <TextView  android:layout_width="match_parent" android:layout_height="50dp" android:layout_gravity="center" android:text="内容" />

            <TextView  android:layout_width="match_parent" android:layout_height="50dp" android:layout_gravity="center" android:text="内容" />

            <TextView  android:layout_width="match_parent" android:layout_height="50dp" android:layout_gravity="center" android:text="内容" />

            <TextView  android:layout_width="match_parent" android:layout_height="50dp" android:layout_gravity="center" android:text="内容" />
        LinearLayout>
    widget.SideTopScrollView>

widget.SideGroupLayout>

实现效果:
这里写图片描述

上面就是实现了ScrollView效果的顶部停留了。
下面把ListView的实现方式,其实跟ScrollView的效果差不多。

代码:

public class SideTopListView extends ListView {

    private OnChildScrollListener mAction;

    public SideTopListView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        return mAction != null && mAction.isChildScroll() && super.onTouchEvent(ev);
    }

    public void setOnChildScrollListener(OnChildScrollListener action) {
        this.mAction = action;
    }

    /** * 判断是否滑动到了第一条数据 */
    public boolean isChildScrollToEnd() {
        if (getFirstVisiblePosition() == 0) {
            View view = getChildAt(0);
            if (view != null) {
                return view.getTop() == getPaddingTop();
            } else {
                return true;
            }
        }
        return false;
    }



}

isChildScrollToEnd()方法是用来判断是否滑动到第一条数据
具体实现:

public class ListViewActivity extends Activity implements OnChildScrollListener, OnGroupScrollListener {

    private SideGroupLayout mHoverLayout;
    private SideTopListView mSideTopListView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        // TODO Auto-generated method stub
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_listview);
        initView();
    }

    private void initView() {
        mHoverLayout = (SideGroupLayout) findViewById(R.id.hoverlayout);
        mSideTopListView = (SideTopListView) findViewById(R.id.listview);
        mHoverLayout.setOnGroupScrollListener(this);
        mSideTopListView.setOnChildScrollListener(this);
        List strs = new ArrayList<>();
        for (int i = 0; i <= 100; i++) {
            strs.add("数据");
        }
        ArrayAdapter mAdapter = new ArrayAdapter(this, android.R.layout.simple_list_item_1, strs);
        mSideTopListView.setAdapter(mAdapter);
    }


    @Override
    public boolean isChildScroll() {
        return mHoverLayout != null && mHoverLayout.isScrollToEnd();
    }

    @Override
    public boolean isGroupScroll() {
        return mSideTopListView != null && mSideTopListView.isChildScrollToEnd();
    }


    @Override
    public void onScrollChanged(int left, int top) {
    }

}

最后看效果:
这里写图片描述

4、总结

其实整个的实现不难,就是一个事件处理过程,当ChildView不需要滑动时,就给GroupView来滑动,当ChildView需要滑动时,就给ChildView来滑动,通过接口的方式来进行链接。

附上Demo


推荐阅读
  • spring boot使用jetty无法启动 ... [详细]
  • 本文介绍了如何通过C#语言调用动态链接库(DLL)中的函数来实现IC卡的基本操作,包括初始化设备、设置密码模式、获取设备状态等,并详细展示了将TextBox中的数据写入IC卡的具体实现方法。 ... [详细]
  • 本文介绍了如何使用 Gesture Detector 和 overridePendingTransition 方法来实现滑动界面和过渡动画。 ... [详细]
  • 在1995年,Simon Plouffe 发现了一种特殊的求和方法来表示某些常数。两年后,Bailey 和 Borwein 在他们的论文中发表了这一发现,这种方法被命名为 Bailey-Borwein-Plouffe (BBP) 公式。该问题要求计算圆周率 π 的第 n 个十六进制数字。 ... [详细]
  • 使用TabActivity实现Android顶部选项卡功能
    本文介绍如何通过继承TabActivity来创建Android应用中的顶部选项卡。通过简单的步骤,您可以轻松地添加多个选项卡,并实现基本的界面切换功能。 ... [详细]
  • 本文详细介绍了 `org.apache.tinkerpop.gremlin.structure.VertexProperty` 类中的 `key()` 方法,并提供了多个实际应用的代码示例。通过这些示例,读者可以更好地理解该方法在图数据库操作中的具体用途。 ... [详细]
  • Beetl是一款先进的Java模板引擎,以其丰富的功能、直观的语法、卓越的性能和易于维护的特点著称。它不仅适用于高响应需求的大型网站,也适合功能复杂的CMS管理系统,提供了一种全新的模板开发体验。 ... [详细]
  • JUnit下的测试和suite
    nsitionalENhttp:www.w3.orgTRxhtml1DTDxhtml1-transitional.dtd ... [详细]
  • 本文将从基础概念入手,详细探讨SpringMVC框架中DispatcherServlet如何通过HandlerMapping进行请求分发,以及其背后的源码实现细节。 ... [详细]
  • Android与JUnit集成测试实践
    本文探讨了如何在Android项目中集成JUnit进行单元测试,并详细介绍了修改AndroidManifest.xml文件以支持测试的方法。 ... [详细]
  • java类名的作用_java下Class.forName的作用是什么,为什么要使用它?
    湖上湖返回与带有给定字符串名的类或接口相关联的Class对象。调用此方法等效于:Class.forName(className,true,currentLoader) ... [详细]
  • CentOS7通过RealVNC实现多人使用服务器桌面
    背景:公司研发团队通过VNC登录到CentOS服务器的桌面实现软件开发工作为防止数据外泄,需要在RealVNC设置禁止传输文件、访问粘贴板等策略过程&# ... [详细]
  • 本文通过C++语言实现了一个递归算法,用于解析并计算数学表达式的值。该算法能够处理加法、减法、乘法和除法操作。 ... [详细]
  • 本文将详细介绍如何使用Java编程语言生成指定数量的不重复随机数,包括具体的实现方法和代码示例。适合初学者和有一定基础的开发者参考。 ... [详细]
  • 本教程介绍如何在C#中通过递归方法将具有父子关系的列表转换为树形结构。我们将详细探讨如何处理字符串类型的键值,并提供一个实用的示例。 ... [详细]
author-avatar
小胖胖的夢2502895687
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有