ScrollerLayout——可横向滚动的自定义viewgroup

具体功能看效果图:

小贴士———如何录屏

使用手机自带的录屏软件,录制后为mp4格式,然后再使用下面这个网址介绍的方法,转为gif(我是用的是在线转的那个,挺好用)

Android手机如何录制屏幕及转GIF

这个自定义控件涉及到的知识点:

  1. 自定义ViewGroup中onMeasure和onLayout的写法
  2. 弹性滚动Scroller的用法
  3. 速度轨迹追踪器VelocityTracker的用法
  4. 如何处理滑动事件冲突
    • dispatchTouchEvent:(外部拦截)告诉此ScrollLayout的父布局,什么时候该拦截触摸事件,什么时候不该拦截触摸事件
    • onInterceptTouchEvent:(内部拦截)ScrollLayout告诉自己什么时候要拦截内部子View的触摸事件,什么时候不要拦截内部子View的触摸事件

代码如下:(注释很全)

package com.github.c.horizonalscrollitem;

import android.content.Context;
import android.support.v4.view.ViewConfigurationCompat;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.widget.Scroller;

/**
 * 注意:此自定义viewgroup只适用于第一个子控件为match_parent的情况,其实一般情况也都是这种情况
 * 注意:此自定义viewgroup,没有考虑padding的情况,使用者不要在ScrollerLayout里使用任何padding,否则你看到的不是你想要的,
 * 为了实现padding效果,你可以为ScrollerLayout的外层再套一层线性布局(或其他布局),在外层布局里使用padding值
 * 此自定义viewgroup基于郭霖博客改编,想了解具体实现细节,请参照:
 * Android Scroller完全解析,关于Scroller你所需知道的一切
 * http://blog.csdn.net/guolin_blog/article/details/48719871
 */
public class ScrollerLayout extends ViewGroup {

    /**
     * 速度轨迹追踪器
     */
    private VelocityTracker mVelocityTracker;

    /**
     * 此次计算速度你想要的最大值
     */
    private final int mMaxVelocity;

    /**
     * 第一个触点的id, 此时可能有多个触点,但至少一个
     */
    private int mPointerId;

    /**
     * 计算出的横向滚动速率
     */
    private float velocityX;

    /**
     * 手指横向滑动的速率临界值,大于这个值时,不考虑手指滑动的距离,直接滚动到最左边或者最右边
     */
    private int criticalVelocityX = 2500;

    /**
     * 用于完成滚动操作的实例
     */
    private Scroller mScroller;

    /**
     * 判定为拖动的最小移动像素数
     */
    private int mTouchSlop;

    /**
     * 手机按下时的屏幕坐标
     */
    private float mXDown;

    /**
     * 手机当时所处的屏幕坐标
     */
    private float mXMove;

    /**
     * 上次触发ACTION_MOVE事件时的屏幕坐标
     */
    private float mXLastMove;

    /**
     * 界面可滚动的左边界
     */
    private int leftBorder;

    /**
     * 界面可滚动的右边界
     */
    private int rightBorder;

    /**
     * 所有子控件的宽度
     */
    private int totalChildWidth;

    /**
     * 第二个子控件的宽度
     */
    private int secondChildWidth;

    /**
     * 最后一个子控件的宽度
     */
    private int lastChildWidth;

    /**
     * 第一个子控件的宽度
     */
    private int firstChildWidth;

    /**
     * 手指是否是向右滑动
     */
    private boolean scrollToRight;

    public ScrollerLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        // 第一步,创建Scroller的实例
        mScroller = new Scroller(context);
        ViewConfiguration configuration = ViewConfiguration.get(context);
        // 获取TouchSlop值
        mTouchSlop = ViewConfigurationCompat.getScaledPagingTouchSlop(configuration);
        //此次计算速度你想要的最大值
        mMaxVelocity = ViewConfiguration.get(context).getMaximumFlingVelocity();
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            View childView = getChildAt(i);
            // 为ScrollerLayout中的每一个子控件测量大小
            measureChild(childView, widthMeasureSpec, heightMeasureSpec);
        }
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        if (changed) {
            int childCount = getChildCount();
            /**
             * 当前子控件之前的所有子控件的总宽度
             */
            int preChildViewTotalWidth = 0;
            for (int i = 0; i < childCount; i++) {
                View childView = getChildAt(i);
                // 为ScrollerLayout中的每一个子控件在水平方向上进行布局
                if (i == 0) {
                    childView.layout(
                            0,
                            0,
                            childView.getMeasuredWidth(),
                            childView.getMeasuredHeight());

                } else {
                    childView.layout(
                            preChildViewTotalWidth,
                            0,
                            preChildViewTotalWidth + childView.getMeasuredWidth(),
                            childView.getMeasuredHeight());
                }
                preChildViewTotalWidth += childView.getMeasuredWidth();

            }
            // 初始化左右边界值
            leftBorder = getChildAt(0).getLeft();
            rightBorder = getChildAt(getChildCount() - 1).getRight();

//            获取第一个控件的宽度
            firstChildWidth = getChildAt(0).getMeasuredWidth();

//            获取第二个控件的宽度,为了让下面在判断在整个布局滑动到第二个控件完全显示后放手时,
//            让布局继续往右滚动,直至滚动显示出所有最右边的所有子控件
            secondChildWidth = getChildAt(1).getMeasuredWidth();
//            获取最后一个控件的宽度,当向左滑动超过最后一个控件的宽度时,
//            整个布局向左滑动,完全显示所有的最左侧的控件
            lastChildWidth = getChildAt(getChildCount() - 1).getMeasuredWidth();
//            整个布局里的所有控件的总宽度:
            totalChildWidth = preChildViewTotalWidth;
//            Log.e("c","preChildViewTotalWidth"+preChildViewTotalWidth+"___rightBorder"+rightBorder);
        }
    }

    private int downX;
    private int downY;
//        告诉此ScrollLayout的父布局,什么时候该拦截触摸事件,什么时候不该拦截触摸事件
    public boolean dispatchTouchEvent(MotionEvent ev) {
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                //让当前ScrollerLayout对应的父控件不要去拦截事件
                getParent().requestDisallowInterceptTouchEvent(true);
                downX = (int) ev.getX();
                downY = (int) ev.getY();
                break;
            case MotionEvent.ACTION_MOVE:
                int moveX = (int) ev.getX();
                int moveY = (int) ev.getY();

                //请求父控件recycleView拦截触摸事件,recycleView上下滚动
                if (Math.abs(moveY - downY) > Math.abs(moveX - downX)) {
                    getParent().requestDisallowInterceptTouchEvent(false);
                } else {
                    // //请求父控件recycleView不要拦截触摸事件,ScrollerLayout自己的可以左右滑动
                    getParent().requestDisallowInterceptTouchEvent(true);
                }

                break;

            case MotionEvent.ACTION_CANCEL:

                break;
            case MotionEvent.ACTION_UP:

                break;
        }
        return super.dispatchTouchEvent(ev);
    }

//      ScrollLayout告诉自己什么时候要拦截内部子View的触摸事件,什么时候不要拦截内部子View的触摸事件
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                //▲▲▲1.求第一个触点的id, 此时可能有多个触点,但至少一个
                mPointerId = ev.getPointerId(0);
                mXDown = ev.getRawX();
                mXLastMove = mXDown;
                break;
            case MotionEvent.ACTION_MOVE:
                mXMove = ev.getRawX();
                float diff = Math.abs(mXMove - mXDown);
                mXLastMove = mXMove;
                // 当手指拖动值大于TouchSlop值时,认为应该进行滚动,拦截子控件的事件
                if (diff > mTouchSlop) {
                    return true;
                }
                break;
        }
        return super.onInterceptTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        //▲▲▲2.向VelocityTracker添加MotionEvent
        acquireVelocityTracker(event);
        switch (event.getAction()) {
            case MotionEvent.ACTION_MOVE:

                //▲▲▲3.求伪瞬时速度
                mVelocityTracker.computeCurrentVelocity(1000, mMaxVelocity);
                velocityX = mVelocityTracker.getXVelocity(mPointerId);

                mXMove = event.getRawX();
                int scrolledX = (int) (mXLastMove - mXMove);//注意取的是负值,因为是整个布局在动,而不是控件在动
//              如果已经在最左侧了,就不让再往左滑动了
                if (getScrollX() + scrolledX < leftBorder) {
                    scrollTo(leftBorder, 0);
                    return true;//★★★★★★★★★★★★★★★★这里返回true或者false实践证明都可以,但是不能什么都不返回。
                } else if (getScrollX() + getWidth() + scrolledX > rightBorder) {//如果已经在最右侧了,就不让再往右滑动了
                    scrollTo(rightBorder - getWidth(), 0);
                    return true;//★★★★★★★★★★★★★★★★★这里返回true或者false实践证明都可以,但是不能什么都不返回。
                }

                scrollBy(scrolledX, 0);//手指move时,布局跟着滚动
                if (mXDown <= mXMove) {//★★★判断手指左滑动,还是右滑动,要用mXDown,而不是mXLastMove
                    scrollToRight = true;//手指往右滑动

                } else {
                    scrollToRight = false;//手指往左滑动
                }
                mXLastMove = mXMove;
                break;
            case MotionEvent.ACTION_UP:
//                4.▲▲▲释放VelocityTracker
                releaseVelocityTracker();
                // 第二步,当手指抬起时,根据当前的滚动值以及滚动方向来判定应该滚动到哪个子控件的界面,并且记得调用invalidate();

                if (Math.abs(velocityX) > criticalVelocityX) {
                    if (scrollToRight) {
                        mScroller.startScroll(getScrollX(), 0, 0 - getScrollX(), 0);
                    } else {
                        mScroller.startScroll(getScrollX(), 0, (totalChildWidth - firstChildWidth) - getScrollX(), 0);
                    }
                } else {
                    if ((getScrollX() >= secondChildWidth && !scrollToRight)//手指往左滑动,并且滑动完全显示第二个控件时,viewgroup滑动到最右端
                            || ((getScrollX() >= (totalChildWidth - firstChildWidth - lastChildWidth) && scrollToRight))) {//手指往右滑动,并且当滑动没有完全隐藏最后一个控件时,viewgroup滑动到最右端
//                    当滚动值大于某个数字时(大于第二个控件的宽度,即完全显示第二个控件时)并且是向左滑动,让这个viewgroup滑动到整个Viewgroup的最右侧,
//                    因为右侧的所有控件宽度是600,而现在已经滑动的距离是getScrollX,
//                    那么,还应该继续滑动的距离是600-getScrollX(),这里正值表示向右滑动
                        mScroller.startScroll(getScrollX(), 0, (totalChildWidth - firstChildWidth) - getScrollX(), 0);
                    } else if ((getScrollX() <= (totalChildWidth - firstChildWidth - lastChildWidth) && scrollToRight)//手指往右滑动,并且当滑动完全隐藏最后一个控件时,viewgroup滑动到最左端
                            || (getScrollX() <= secondChildWidth && !scrollToRight)) {//手指往左滑动,并且滑动没有完全显示第二个控件时,viewgroup滑动到最左端

//                    当滚动值小于某个数字时,让这个viewgroup滑动到整个Viewgroup的最左侧,
//                    因为滑动到最左侧时,就是让整个viewgroup的滑动量为0,而现在已经滑动的距离是getScrollX,
//                    那么,还应该继续滑动的距离是0-getScrollX(),这里负值表示向左滑动
                        mScroller.startScroll(getScrollX(), 0, 0 - getScrollX(), 0);
                    }
                }
//                必须调用invalidate()重绘
                invalidate();

                break;

            case MotionEvent.ACTION_CANCEL:
//              5.▲▲▲释放VelocityTracker
                releaseVelocityTracker();
                break;
        }
        return super.onTouchEvent(event);
    }

    @Override
    public void computeScroll() {
        // 第三步,重写computeScroll()方法,并在其内部完成平滑滚动的逻辑
        if (mScroller.computeScrollOffset()) {
            scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
            invalidate();
        }
    }

    /**
     * @param event 向VelocityTracker添加MotionEvent
     * @see android.view.VelocityTracker#obtain()
     * @see android.view.VelocityTracker#addMovement(MotionEvent)
     */
    private void acquireVelocityTracker(final MotionEvent event) {
        if (null == mVelocityTracker) {
            mVelocityTracker = VelocityTracker.obtain();
        }
        mVelocityTracker.addMovement(event);
    }

    /**
     * 释放VelocityTracker
     *
     * @see android.view.VelocityTracker#clear()
     * @see android.view.VelocityTracker#recycle()
     */
    private void releaseVelocityTracker() {
        if (null != mVelocityTracker) {
            mVelocityTracker.clear();
            mVelocityTracker.recycle();
            mVelocityTracker = null;
        }
    }

     /*   getScrollX()指的是由viewgroup调用View的scrollTo(int x, int y)或者scrollBy(int x, int y)产生的X轴的距离
//                换句话说,就是你手指每次滑动,引起的是viewgroup累计滑动的距离,右为正
//                指的是相当于控件的左上角的为原点的坐标值
                Log.e("qqq","getX():"+event.getX());
//                指的是相当于屏幕的左上角的为原点的坐标值
                Log.e("qqq","getRawX():"+event.getRawX());*/
}
时间: 2024-11-07 11:58:02

ScrollerLayout——可横向滚动的自定义viewgroup的相关文章

ListView的下拉刷新上拉加载以及带列的横向滚动

相信有很人做的项目估计都用的到这个.就是ListView的下拉刷新上拉加载还有就是列的横向滚动; PS:横向滚动带表头与固定列(相信蛮多人都有这样的需求吧?就是在ListView上支持很多列,然而设备屏幕宽度有限) PS:这是我个人在网上找的两个示例demo结合而成的一个示例demo,还可以继续拓展,后续有时间就会更新,大家互相学习 ListView下拉刷新上拉加载示例demo原文出处: http://blog.csdn.net/limb99/article/details/18901513 L

Android ViewDragHelper完全解析 自定义ViewGroup神器

转载请标明出处: http://blog.csdn.net/lmj623565791/article/details/46858663: 本文出自:[张鸿洋的博客] 一.概述 在自定义ViewGroup中,很多效果都包含用户手指去拖动其内部的某个View(eg:侧滑菜单等),针对具体的需要去写好onInterceptTouchEvent和onTouchEvent这两个方法是一件很不容易的事,需要自己去处理:多手指的处理.加速度检测等等. 好在官方在v4的支持包中提供了ViewDragHelper

Android TextView 横向滚动(跑马灯效果)

Android TextView 中当文字比较多时希望它横向滚动显示,下面是一种亲测可行的方法. 效果图: 1.自定义TextView,重写isFocused()方法返回true,让自定义TextView一直处于获取焦点状态. package com.example.shen.marqueedemo; import android.content.Context; import android.util.AttributeSet; import android.widget.TextView;

Android 自定义ViewGroup,实现侧方位滑动菜单

侧方位滑动菜单 1.现在adnroid流行的应用当中很多都是用的侧方位滑动菜单如图: 将菜单显示在左边,内容页面显示在右边,通过滑动或则按钮点击来隐藏和显示菜单. 2.首先对ViewGroup进行个了解: View是ViewGroup的父类,ViewGroup具有View的所有特性,ViewGroup主要用用来充当View的容器,将其中的View作为自己孩子, 并对其进行管理,当然孩子也是可以是ViewGroup类型. View类一般用于绘图操作,重写他的onDraw方法,但它不可以包含其他组件

Android 自定义ViewGroup之实现FlowLayout-标签流容器

本篇文章讲的是Android 自定义ViewGroup之实现标签流式布局-FlowLayout,开发中我们会经常需要实现类似于热门标签等自动换行的流式布局的功能,网上也有很多这样的FlowLayout,但不影响我对其的学习.和往常一样,主要还是想总结一下自定义ViewGroup的开发过程以及一些需要注意的地方. 按照惯例,我们先来看看效果图 一.写代码之前,有几个是问题是我们先要弄清楚的: 1.什么是ViewGroup:从名字上来看,它可以被翻译为控件组,言外之意是ViewGroup内部包含了许

[Android Pro] Android开发实践:自定义ViewGroup的onLayout()分析

reference to : http://www.linuxidc.com/Linux/2014-12/110165.htm 前一篇文章主要讲了自定义View为什么要重载onMeasure()方法(见 http://www.linuxidc.com/Linux/2014-12/110164.htm),那么,自定义ViewGroup又都有哪些方法需要重载或者实现呢 ? Android开 发中,对于自定义View,分为两种,一种是自定义控件(继承View类),另一种是自定义布局容器(继承ViewG

【Android自定义ViewGroup】不一样的轮子,巧用类变量解决冲突,像IOS那样简单的使用侧滑删除,一个控件搞定Android item侧滑删除菜单。

================================================================================== [1 序言] 侧滑删除的轮子网上有很多,最初在github上看过一个,还是ListView时代,那是一个自定义ListView 实现侧滑删除的,当初就觉得这种做法不是最佳,万一我项目里又同时有自定义ListView的需求,会增加复杂度. 写这篇文章之前又通过毒度搜了一下,排名前几的CSDN文章,都是通过自定义ListVIew和Vie

Android自定义ViewGroup打造各种风格的SlidingMenu

看鸿洋大大的QQ5.0侧滑菜单的视频课程,对于侧滑的时的动画效果的实现有了新的认识,似乎打通了任督二脉,目前可以实现任意效果的侧滑菜单了,感谢鸿洋大大!! 鸿洋大大用的是HorizontalScrollView来实现的侧滑菜单功能,HorizontalScrollView的好处是为我们解决了滑动功能,处理了滑动冲突问题,让我们使用起来非常方便,但是滑动和冲突处理都是android中的难点,是我们应该掌握的知识点,掌握了这些,我们可以不依赖于系统的API,随心所欲打造我们想要的效果,因此这篇文章我

QQ 5.0侧滑HorizontalScrollView以及自定义ViewGroup

一般侧滑的实现: 自定义的ViewGroup(menu+content) ouTouchEvent事件改变ViewGroup的LeftMargin. 大于菜单的一半显示,小于则隐藏(使用Scroller或者LeftMargin加Thread). 新的方法: 继承HorizontalScrollView,由于menu+content一定是大于屏幕的宽度,那么水平滚动条会自动进行拖动, 我们只要做偏移的距离还显示或隐藏menu或content. 1.最简单的侧滑 自定义ViewGroup即Horiz