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

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
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
MagicIndicator是一个强大的指示器库,可以帮助我们轻松地实现各种样式的ViewPager指示器。本文将介绍如何使用MagicIndicator打造千变万化的ViewPager指示器。 1. 引入依赖库 在app的build.gradle文件中添加以下依赖: ``` dependencies { implementation 'com.github.hackware1993:MagicIndicator:1.7.0' } ``` 2. 在布局文件中添加MagicIndicator 我们可以在布局文件中添加一个MagicIndicator作为ViewPager的指示器,例如: ``` <com.github.chenglei1986.navigationbarlib.MagicIndicator android:id="@+id/magic_indicator" android:layout_width="match_parent" android:layout_height="48dp"/> ``` 3. 创建ViewPager 我们需要创建一个ViewPager,并将其与MagicIndicator进行绑定。例如: ``` ViewPager viewPager = findViewById(R.id.view_pager); viewPager.setAdapter(adapter); MagicIndicator magicIndicator = findViewById(R.id.magic_indicator); magicIndicator.setNavigator(navigator); ViewPagerHelper.bind(magicIndicator, viewPager); ``` 在上面的代码中,我们创建了一个ViewPager,并将其与适配器进行绑定。然后,我们创建了一个MagicIndicator,并将其与ViewPager进行绑定。我们还使用了ViewPagerHelper类来将MagicIndicator和ViewPager绑定在一起。 4. 创建导航器 导航器是用来控制ViewPager指示器的样式和行为的。我们可以使用CommonNavigator类来创建一个导航器,例如: ``` CommonNavigator navigator = new CommonNavigator(this); navigator.setAdapter(new CommonNavigatorAdapter() { @Override public int getCount() { return titles.length; } @Override public IPagerTitleView getTitleView(Context context, int index) { ColorTransitionPagerTitleView titleView = new ColorTransitionPagerTitleView(context); titleView.setNormalColor(Color.GRAY); titleView.setSelectedColor(Color.WHITE); titleView.setText(titles[index]); titleView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { viewPager.setCurrentItem(index); } }); return titleView; } @Override public IPagerIndicator getIndicator(Context context) { LinePagerIndicator indicator = new LinePagerIndicator(context); indicator.setColors(Color.WHITE); return indicator; } }); ``` 在上面的代码中,我们创建了一个CommonNavigator,并设置了它的适配器。适配器中,我们需要实现getCount方法返回指示器的数量,getTitleView方法返回每个指示器的视图,getIndicator方法返回指示器的样式。 在本例中,我们使用ColorTransitionPagerTitleView来创建指示器的视图,它可以在切换选中状态时自动进行颜色过渡。我们还使用LinePagerIndicator来创建指示器的样式,它是一条横线,颜色为白色。 5. 运行程序 现在,我们已经完成了MagicIndicator的设置,可以运行程序并查看效果。您可以尝试更改导航器的样式,例如更改指示器的颜色、大小和形状等,以实现不同的效果。 总结 在本文中,我们介绍了如何使用MagicIndicator打造千变万化的ViewPager指示器。通过使用MagicIndicator,我们可以轻松地创建不同样式的ViewPager指示器,从而提高用户体验。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值