Android自定义view之ViewPager指示器——2

来源:互联网 发布:如何编写电脑软件 编辑:程序博客网 时间:2024/05/18 02:17

Android自定义view之ViewPager指示器——2

上一篇《Android自定义view之ViewPager指示器——1》中我们一起写了测量和布局的流程。本篇我们继续讲解事件分发,以及其他的功能性方法。

5. 事件分发

按照之前我们讲的事件分发流程,作为一个ViewGroup,按照顺序,首先来到的是onInterceptTouchEvent()方法。

    @Override    public boolean onInterceptTouchEvent(MotionEvent ev) {        return true;    }

这个方法是决定当前的ViewGroup是否要拦截触摸事件。我们想要自己处理这些事件,所以返回true即可。
接下来事件就会到达onTouchEvent()方法。

    /*本次事件流是否已经被判断为滑动事件*/    private boolean moved = false;    @Override    public boolean onTouchEvent(MotionEvent event) {        switch (event.getAction())        {            /*按下的时候,对各个值进行赋值*/            case MotionEvent.ACTION_DOWN:                newX = event.getRawX();                newY = event.getRawY();                downX = newX;                downY = newY;                break;            /*发生滑动时,先更新值,然后用前一次触摸点的坐标和本次坐标进行计算,如果x方向上的移动距离大于touchSlop,那么            * 就判断为滑动。*/            case MotionEvent.ACTION_MOVE:                lastX = newX;                lastY = newY;                newX = event.getRawX();                newY = event.getRawY();                dx = newX - lastX;                dy = newY - lastY;                if(dx >= touchSlop)                {                    moved = true;                }                /*判断TextView的布局。tags太多的话,是允许滑动的,但是滑动也有限制,第一个tag的最左边不可以大于指示器的paddingLeft。                * 最后一个tag的最右边不可以小于(height - paddingRight)。然后以这个条件来计算滑动后的第一个tag最左边的位置。                * 并进行重新布局,横线的位置及长短也要相应改变。*/                int left = tagMap.get(tags.get(0)).getLeft();                int right = tagMap.get(tags.get(tags.size() - 1)).getRight();                int length = right - left;                if (length < getMeasuredWidth() - getPaddingLeft() - getPaddingRight())                {                }else if(left + dx > getPaddingLeft())                {                    left = getPaddingLeft();                }else if(right + dx < (getMeasuredWidth() - getPaddingRight()))                {                    right = getMeasuredWidth() - getPaddingRight();                    left = right - length;                }else                {                    left += dx;                }                textOffset = left;                layoutChildren(left, indicatorLine.getLeft() + (left - tagMap.get(tags.get(0)).getLeft()), indicatorLine.getWidth());                break;            /*抬起时,判断从起点到落点的距离是否超过了touchSlop,如果不是,我们就判断它是点击事件,执行点击回调函数。否则就什么也不做。另外            * 将moved设为false,收尾。*/            case MotionEvent.ACTION_UP:                lastX = newX;                lastY = newY;                newX = event.getRawX();                newY = event.getRawY();                dx = newX - lastX;                dy = newY - lastY;                /*判断为点击事件*/                if(!moved && Math.abs(newX - downX) < touchSlop && Math.abs(newY - downY) < touchSlop)                {                    for(int i = 0; i < tags.size(); i++)                    {                        TextView child = tagMap.get(tags.get(i));                        if(child.getLeft() <= newX && child.getRight() >= newX)                        {                            notifyOnTagClickedListsners(i);                            break;                        }                    }                }                moved = false;                break;        }        return super.onTouchEvent(event);    }

首先,我们有个变量moved,来判断当前事件流中是否出现过满足条件的滑动事件。根据这个情况我们可以在ACTION_UP时判断是否要判断为点击事件并调用点击函数。
方法中出现的几个变量需要说一下:newX和newY是新的事件的坐标;lastX和lastY是上一个事件的坐标;downX和downY是ACTION_DOWN事件的坐标。
另外有几个点需要说一下:
1. 在滑动事件的判断中,我们以前一次和本次事件的坐标差来判断,其实也可以再加入以本次事件和ACTION_DOWN事件坐标值差来判断,毕竟前者只计算相邻的两次事件的坐标差,如果用户缓慢移动的话,那可能滑了很久也不会被判断为移动,如果两种结合起来,判断会更为准确。
2. 在ACTION_MOVE的case中调用layoutChildren的时机问题。严格来说应该在确实判断为滑动之后才调用来重新布局。不过这个控件因为体量较小,并且目前这种策略也没有什么问题。但是如果在写比如ListView这种大体量并且触摸操作很频繁的控件时还是要严格一点。
3. 没有对多指触摸进行优化,这会导致多指操作时出现跳动的情况。这在可滑动控件中是个比较严重的问题,不过好在指示器也并没有双指操作的设定。

我们主要的工作是在ACTION_MOVE的case里进行的,其实就是根据手指的移动距离来布局子view。onTouchEvent方法是需要返回一个boolean的,我们直接返回了父类的onTouchEvent()的返回值。这里其实是View类的onTouchEvent()方法,因为ViewGroup并没有重写这个方法。在View类中查看这个方法,我们发现只要这个View是可点击的,无论单击还是长按,这个方法就会返回true。而如果你仔细看了我们的构造函数,就会看到在里面我们写了this.setClickable(true)。简而言之就是要在这里返回一个true表示我们消费了这个事件,所以你可以直接返回true。不过这里另外讲了一个点而已。

6. 与ViewPager交互

现在触摸事件也已经完成了,不过还没完,因为指示器肯定是需要和ViewPager做一些交互的,根据当前ViewPager的位置来进行变化。那我们就写一个监听ViewPager变化的方法。

    /**使用在ViewPager.OnPageChangeListener.onPageScrolled(int position, float positionOffset, int positionOffsetPixels)    * 方法中,将三个参数原样传到该函数即可。     * @param position 当前可见的第一个页面的序号,如果positionOffset不为0,那么position + 1页面也是可见的。     * @param positionOffset 取值范围[0, 1),表示当前position页面的偏离范围。     * @param positionOffsetPixels 当前position页面的偏离值。    * */    public void listen(int position, float positionOffset, int positionOffsetPixels)    {        currentPosition = position;        /*需要让被选中的tag完整地显示出来,因此在tag布局在指示器的显示范围之外时需要移动,并且修改选中的和未选中的字的颜色。*/        if(positionOffset != 0.0f)        {            TextView current = tagMap.get(tags.get(position));            TextView old = tagMap.get(tags.get(position + 1));            int spaceBetweenTags = old.getLeft() - current.getLeft();            int lineLeft = current.getLeft() + (int)(spaceBetweenTags * positionOffset);            int currentLength = current.getWidth();            int oldLength = old.getWidth();            int lineLength = currentLength + (int)((oldLength - currentLength) * positionOffset);            int lineRight = lineLeft + lineLength;            if(lineLength >= (getMeasuredWidth() - getPaddingLeft() - getPaddingRight()))            {                textOffset += getPaddingLeft() - lineLeft;                lineLeft = getPaddingLeft();            }else if(lineLeft < getPaddingLeft())            {                textOffset += getPaddingLeft() - lineLeft;                lineLeft = getPaddingLeft();//                lineLength = lineRight - lineLeft;            }else if(lineRight > (getMeasuredWidth() - getPaddingRight()))            {                textOffset -= lineRight - (getMeasuredWidth() - getPaddingRight());                lineRight = (getMeasuredWidth() - getPaddingRight());//                lineLength = lineRight - lineLeft;                lineLeft = lineRight - lineLength;            }            layoutChildren(textOffset, lineLeft, lineLength);            current.setTextColor(evaluateColor(textColor, selectedTextColor, 1 - positionOffset));            old.setTextColor(evaluateColor(textColor, selectedTextColor, positionOffset));        }else        {            /*positionOffset == 0时说明此时已经完成了页面切换,有且仅有一个页面是被完整显示的,此时只要根据被选择的序号来布局即可。关于颜色            * 改变,因为在positionOffset == 0时我们已经丢失了页面切换的信息,所以无法得知上一个被选中的页面是哪个。另外,positionOffset == 0            * 的情况实际上很极端,因此对于这种情况的处理并不影响大局,连这种情况的位置布局都可以不必考虑*/            TextView current = tagMap.get(tags.get(position));            if(current.getLeft() < getPaddingLeft())            {                textOffset += getPaddingLeft() - current.getLeft();            }else if(current.getRight() > getMeasuredWidth() - getPaddingRight())            {                textOffset += getMeasuredWidth() - getPaddingRight() - current.getRight();            }            layoutChildren(textOffset, position);        }    }

上面的函数就是和ViewPager交互的主要函数,它可以监听ViewPager的页面转换信息并且同步改变布局。注意positionOffset == 0.0f的情况是极端的,它只会在整个变换过程中占非常小的占比,因此对于它的处理是不影响大局的,即使将else分支里的代码整个删除也不会有问题。
在这里主要是变化tag和横线的位置,特殊情况是即将被选中的tag没有被完全显示,此时我们就需要根据情况改变tag的位置,使得最终被选中的tag能够完全显示。由于tag不一定都是等长的,因此横线的宽度也要根据变化的百分比进行改变,同样的还有文字的颜色。这里的positionOffset事实上就是变化的百分比,但是它并不是总是按照从0到1的顺序变化,为了方便,下面举个例子:
1. 如果当前显示的页面1号页面,然后我们向左滑动,想要显示2号页面。在没有滑动时,position == 1,positionOffset == 0。在滑动开始只到2号页面完全显示出来时,position == 1,而positionOffset则是从0到1开始变化(并不会到达1)。到2号页面完全显示出来,页面切换完成时,position == 2,positionOffset == 0。
2. 如果向右滑动,想要显示0号页面,在没有滑动时,情况和上面是一样的。在滑动过程中,position == 0,而positionOffset则是从1到0开始减小(并不会等于1)。到0号页面完全显示出来时,position == 0,positionOffset == 0。

可以看出position总是显示的第一个页面的序号,而positionOffset就是显示的第一个页面到后一个页面的变化的百分比。

7. 其他的功能性函数

添加删除tag

    public void addTags(String[] tags)    {        for(String s : tags)        {            addTag(s);        }    }    public void addTag(String tag, int... index)    {        log.v("add tag");        /*新建TextView以容纳tag,并且TextView的宽和高都是WRAP_CONTENT的*/        TextView t = new TextView(getContext());        ViewGroup.MarginLayoutParams layoutParams = new ViewGroup.MarginLayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);        t.setLayoutParams(layoutParams);        t.setText(tag);        t.setTextColor(textColor);        t.setTextSize(textSize);        t.setPadding(2,2,2,2);        if(index != null && index.length != 0)        {            if(index[0] <= currentPosition)            {                currentPosition++;            }            this.tags.add(index[0], tag);        }else        {            this.tags.add(tag);        }        this.tagMap.put(tag, t);        this.addView(t);    }    public void removeTag(String tag)    {        tags.remove(tag);        tagMap.remove(tag);        removeAllViews();        layoutChildren(textOffset, currentPosition);    }

获取当前选中tag的序号

    public int getCurrentPosition()    {        return currentPosition;    }

监听器相关

    public void addOnTagClickedListener(OnTagClickedListener l)    {        if(!onTagClickedListeners.contains(l))        {            onTagClickedListeners.add(l);        }    }    public void removeOnTagClickedListener(OnTagClickedListener l)    {        if(onTagClickedListeners.contains(l))        {            onTagClickedListeners.remove(l);        }    }    /*回调监听器的方法,以当前被点击的tag序号为参数*/    private void notifyOnTagClickedListsners(int position)    {        if(onTagClickedListeners.size() != 0)        {            for(OnTagClickedListener l : onTagClickedListeners)            {                l.onTagClicked(position);            }        }    }    /**tag点击监听器接口*/    public interface OnTagClickedListener    {        public void onTagClicked(int position);    }

颜色转换
颜色要各通道单独变化才可以,如果是将颜色整体作为一个int值来变化,那么中间就会出现其他颜色。

    private int evaluateColor(int fromColor, int toColor, float percent)    {        int fromA = (fromColor >> 24) & 0xff;        int fromR = (fromColor >> 16) & 0xff;        int fromG = (fromColor >> 8) & 0xff;        int fromB = fromColor & 0xff;        int toA = (toColor >> 24) & 0xff;        int toR = (toColor >> 16) & 0xff;        int toG = (toColor >> 8) & 0xff;        int toB = toColor & 0xff;        int dA = (int)((toA - fromA) * percent);        int dR = (int)((toR - fromR) * percent);        int dG = (int)((toG - fromG) * percent);        int dB = (int)((toB - fromB) * percent);        int color = ((fromA + dA) << 24) | ((fromR + dR) << 16) | ((fromG + dG) << 8) | ((fromB + dB));        return color;    }

8. 使用方法

按照正常控件的使用方法即可。初始化后添加tag

indicator = (TextViewPagerIndicator)findViewById(R.id.view_pager_indicator);indicator.addTag(tag);//可添加多个tag

然后设置各种回调函数即可。

        viewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {            @Override            public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {                indicator.listen(position, positionOffset, positionOffsetPixels);            }            @Override            public void onPageSelected(int position) {            }            @Override            public void onPageScrollStateChanged(int state) {            }        });        indicator.addOnTagClickedListener(new TextViewPagerIndicator.OnTagClickedListener() {            @Override            public void onTagClicked(int position) {                viewPager.setCurrentItem(position, true);            }        });

9. 总结

这次我们真正从0开始自定义了一个指示器,用到了之前我们讲过的很多知识点。虽然这个自定义view还有一些瑕疵,但是整体已经可以使用了。这是我早期写的一个自定义view,后面会有比这个更加复杂的。有的部分分析写得比较简单,但是代码中的注释写得很明白的,并不复杂。

感兴趣的同学也可以上我的Github上查看,其中的CustomView工程有很多更复杂更高级的自定义view。也欢迎大家关注一波。源码也在这个工程中。

阅读全文
0 0
原创粉丝点击