ChangeTabLayout实现过程

原创 2017年04月01日 10:14:30

本文已授权微信公众号:鸿洋(hongyangAndroid)原创首发。

ChangeTabLayout是我模仿乐视LIVE App主界面的TabLayout效果实现的,希望大家多多支持。

1.效果展示与说明

原效果图

原效果图转为Gif过大,所以将录制的MP4效果视频已经放入了项目根目录的preview文件夹内,有兴趣可去查看。(高清无码哦~)

实现效果图

这里写图片描述

ChangeTabLayout在打开状态时

  • 垂直方向切换时,文字的颜色大小变化。
  • 水平方向切换时,文字的渐变与图片的变化。

ChangeTabLayout在收起状态时

  • 垂直方向切换时,图片的变化。
  • 点击ChangeTabLayout,切换为打开状态。

2.分析

首先利用HierarchyViewer查看一下层级:

这里写图片描述

上图我们可以知道,TabLayout是一个ScrollView,内容区域则是垂直ViewPager嵌套了一个水平方向的ViewPager。图片颜色的变化则是使用两个ImageView叠加实现的。知道了这些,我们的思路大致就有了。当然我们不一定完全一样,可以按自己的方式处理。

最后贴一张我实现的最终效果:

这里写图片描述

可以看到我的结构会比较简洁一些,因为图片部分的效果我使用了自定义Drawable去实现,所以不需在叠加一个ImageView,也就少了外层的FrameLayout,其次指示器我是用Canvas去绘制的。所以少了外层的RelativeLayout

3.准备工作

  • 上面我们提到有用到了垂直方向滑动的ViewPager,那么我顺利的在传说中最大的“同性交友网站”Github上找到了VerticalViewPager,可惜此项目年代久远,比如setOnPageChangeListener已经过时,而有时我们需添加多个监听器,能同时生效。所以我参考了VerticalViewPager的思路,重新对现有的ViewPager(25.1.0)源码进行了修改。(真是个细致活)

  • 其次我想起了我曾经用到的SmartTabLayout,觉得使用起来很便捷。所以提前阅读了它的源码。所以此项目的实现结构大量的借鉴了它。

  • 对于图片的变化部分,我找到了这篇自定义Drawables在研究了代码之后,根据需求在此基础上添加了垂直方向的判断,去除了多余的代码部分。

感谢以上作者的分享!那么万事具备,开搞!!

4.实现流程

在准备工作之后,首先明确我们还缺什么,那么剩余的就是文字部分、指示器部分、与承载这些组件的容器了。

1.文字部分

根据观察效果图,文字的变化是被指示器覆盖的部分,文字变为白色。且在页面垂直移动时,文字会有大小的变化。当然页面水平切换时,文字的渐变我们可以利用setAlpha去实现。

ChangeTextView核心代码:

public ChangeTextView(Context context,  AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        mTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
        mTextPaint.setTextAlign(Paint.Align.LEFT);

        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setStyle(Paint.Style.FILL_AND_STROKE);
        PorterDuffXfermode mode = new PorterDuffXfermode(PorterDuff.Mode.SRC_IN);
        mPaint.setXfermode(mode);

    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        resetting();

        Bitmap srcBitmap = Bitmap.createBitmap(getMeasuredWidth(), getMeasuredHeight(), Bitmap.Config.ARGB_8888);
        Canvas srcCanvas = new Canvas(srcBitmap);

        RectF rectF;
        //文字随指示器位置进行颜色变化
        if (level == 10000 || level == 0) {
            rectF = new RectF(0, 0, 0, 0);
        }else if (level == 5000) {
            rectF = new RectF(0, 0, getMeasuredWidth(), getMeasuredHeight());
        }else{
            float value = (level / 5000f) - 1f;

            if(value > 0){
                rectF = new RectF(0, getMeasuredHeight() * value + indicatorPadding, getMeasuredWidth(), getMeasuredHeight());
            }else{
                rectF = new RectF(0, 0, getMeasuredWidth(), getMeasuredHeight() * (1 - Math.abs(value)) - indicatorPadding);
            }
        }

        srcCanvas.save();
        srcCanvas.translate(0, (getMeasuredHeight() - mStaticLayout.getHeight()) / 2);
        mStaticLayout.draw(srcCanvas);
        srcCanvas.restore();

        mPaint.setColor(selectedTabTextColor);
        srcCanvas.drawRect(rectF, mPaint);
        canvas.drawBitmap(srcBitmap, 0, 0, null);
    }

    private void resetting(){
        float size;
        //字体随滑动变化
        if (level == 5000) {
            size = textSize * 1.1f; //最大为默认大小的1.1倍
        }else if(level == 10000 || level == 0){
            size = textSize * 1f;
        }else{
            float value = (level / 5000f) - 1f;
            size = textSize + textSize * (1 - Math.abs(value))* 0.1f;
        }

        mTextPaint.setTextSize(size);
        mTextPaint.setColor(defaultTabTextColor);
        int num = (getMeasuredWidth() - indicatorPadding) / (int) size; // 一行可以放下的字数,默认放置两行文字

        mStaticLayout = new StaticLayout(text, 0, text.length() > num * 2 ?  num * 2 : text.length(), mTextPaint, getMeasuredWidth() - indicatorPadding,
                Layout.Alignment.ALIGN_NORMAL, 1.0F, 0.0F, false);

    }

计算部分就不介绍了,文字的变化主要利用了我们常见的PorterDuffXfermodeSRC_IN模式。也就是取两层绘制交集,显示上层。如下图:

这里写图片描述

比如文字是黑色,这个遮罩是淡蓝色,那么重叠部分的文字就会变为淡蓝色。同时其余遮罩部分不显示。如下示意图:

这里写图片描述

那么我们变化RectF 的大小就可以控制文字的颜色变化。

那么为了可以显示多行文字,同时让文字可以随大小变化自动换行。我使用了StaticLayout去实现。使用起来很简单方便。

2.指示器部分

这里的指示器、背景、阴影部分都放到了ScrollView的子容器LinearLayout中。

ChangeTabStrip核心代码:

class ChangeTabStrip extends LinearLayout{

    public ChangeTabStrip(Context context, @Nullable AttributeSet attrs) {
        super(context);
        setWillNotDraw(false);
        setOrientation(VERTICAL);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        drawShadow(canvas);
        drawBackground(canvas);
        drawDecoration(canvas);
    }

    private void drawDecoration(Canvas canvas) {
        final int tabCount = getChildCount();

        if (tabCount > 0) {
            View selectedTab = getChildAt(selectedPosition);
            int selectedTop = selectedTab.getTop();
            int selectedBottom = selectedTab.getBottom();
            int top = selectedTop;
            int bottom = selectedBottom;

            if (selectionOffset > 0f && selectedPosition < (getChildCount() - 1)) {

                View nextTab = getChildAt(selectedPosition + 1);
                int nextTop = nextTab.getTop();
                int nextBottom = nextTab.getBottom();
                top = (int) (selectionOffset * nextTop + (1.0f - selectionOffset) * top);
                bottom = (int) (selectionOffset * nextBottom + (1.0f - selectionOffset) * bottom);
            }
            drawIndicator(canvas, top, bottom);
        }

    }

    /**
     * 绘制左边阴影
     */
    private void drawShadow(Canvas canvas){
        final float width = shadowWidth * (1 - selectionOffsetX);
        LinearGradient linearGradient = new LinearGradient(0, getHeight(), width, getHeight(), new int[] {shadowColor, Color.TRANSPARENT}, new float[]{shadowProportion, 1f}, Shader.TileMode.CLAMP);
        shadowPaint.setShader(linearGradient);
        canvas.drawRect(0, 0, width, getHeight(), shadowPaint);
    }

    /**
     * 绘制背景
     */
    private void drawBackground(Canvas canvas){
        final float width = getWidth() * selectionOffsetX;
        canvas.drawRect(0, 0, width, getHeight(), backgroundPaint);
    }

    /**
     * 绘制指示器
     */
    private void drawIndicator(Canvas canvas, int top, int bottom) {

        final float width = getWidth() * selectionOffsetX;
        top = top + indicatorPadding;
        bottom = bottom - indicatorPadding;

        float leftBorderThickness = this.leftBorderThickness - getWidth() * (1 - selectionOffsetX);
        if(leftBorderThickness < 0){
            leftBorderThickness = 0;
        }

        borderPaint.setColor(leftBorderColor);
        canvas.drawRect(0, top, leftBorderThickness, bottom, borderPaint);

        indicatorPaint.setColor(indicatorColor);
        indicatorRectF.set(leftBorderThickness, top, width, bottom);

        canvas.drawRect(indicatorRectF, indicatorPaint);
    }


}

这里没有什么特别的,主要就是根据ViewPager的移动不断绘制新的位置。

3.TabLayout部分

首先创建子容器:

ChangeTabStrip tabStrip = new ChangeTabStrip(context, attrs);
addView(tabStrip , LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);

根据传入的PagerAdapter,利用adapter.getCount()方法创建相应数量的TabView,并添加至ChangeTabStrip。简化代码如下:

 private void populateTabStrip() {
        final PagerAdapter adapter = viewPager.getAdapter();

        int size = adapter.getCount();
        for (int i = 0; i < size; i++) {
            LinearLayout tabView = createTabView(adapter.getPageTitle(i), icon[i], 0);

            if (tabView == null) {
                throw new IllegalStateException("tabView is null.");
            }

            tabStrip.addView(tabView);

            if (i == viewPager.getCurrentItem()) { //当前Page对应TabView为选中状态
                ChangeTextView textView = (ChangeTextView) tabView.getChildAt(1);
                textView.setLevel(5000);
            }

        }
    }

protected LinearLayout createTabView(CharSequence title, int icon) {

        LinearLayout mLinearLayout = new LinearLayout(getContext());
        mLinearLayout.setLayoutParams(new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, tabViewHeight));

        ImageView imageView = new ImageView(getContext());

        RevealDrawable drawable = new RevealDrawable(DrawableUtils.getDrawable(getContext(), icon), DrawableUtils.getDrawable(getContext(), selectIcon), RevealDrawable.VERTICAL);       

        imageView.setImageDrawable(drawable);

        ChangeTextView textView = new ChangeTextView(getContext(), attrs);
        textView.setText(title.toString());        

        mLinearLayout.addView(imageView);
        mLinearLayout.addView(textView);
        return mLinearLayout;
}

监听垂直方向ViewPager

viewPager.addOnPageChangeListener(new InternalViewPagerListener());


private class InternalViewPagerListener implements VerticalViewPager.OnPageChangeListener {

        private int scrollState;

        @Override
        public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
            int tabStripChildCount = tabStrip.getChildCount();
            if ((tabStripChildCount == 0) || (position < 0) || (position >= tabStripChildCount)) {
                return;
            }
            tabStrip.onViewPagerPageChanged(position, positionOffset); // 控制指示器位置
            scrollToTab(position, positionOffset); //滚动ScrollView到对应位置
        }

        @Override
        public void onPageScrollStateChanged(int state) {
            scrollState = state;
        }

        @Override
        public void onPageSelected(int position) {
            if (scrollState == ViewPager.SCROLL_STATE_IDLE) {
                scrollToTab(position, 0);
            }
            page = position; // 记录位置

            //更改文字的显示
            for (int i = 0, size = tabStrip.getChildCount(); i < size; i++) {
                ChangeTextView textView = (ChangeTextView) ((LinearLayout) tabStrip.getChildAt(i)).getChildAt(1);
                if (position == i) {
                    textView.setLevel(5000);
                }else {
                    textView.setLevel(0);
                }
            }
        }
    }

private void scrollToTab(int tabIndex, float positionOffset) {

        final int tabStripChildCount = tabStrip.getChildCount();
        if (tabStripChildCount == 0 || tabIndex < 0 || tabIndex >= tabStripChildCount) {
            return;
        }

        LinearLayout selectedTab = (LinearLayout) getTabAt(tabIndex);

        int titleOffset = tabViewHeight * 2;
        int extraOffset = (int) (positionOffset * selectedTab.getHeight());

        int y = (tabIndex > 0 || positionOffset > 0) ? -titleOffset : 0;
        int start = selectedTab.getTop();
        y += start + extraOffset;

        scrollTo(0, y);
    }

垂直滑动时图片,文字的动态变化部分:

private void scrollToTab(int tabIndex, float positionOffset) {

        LinearLayout selectedTab = (LinearLayout) getTabAt(tabIndex);

        if (0f <= positionOffset && positionOffset < 1f) {
            if(!tabLayoutState){ // 关闭状态图片变化
                ImageView imageView = (ImageView) selectedTab.getChildAt(0);
                ((RevealDrawable)imageView.getDrawable()).setOrientation(RevealDrawable.VERTICAL);
                imageView.setImageLevel((int) (positionOffset * 5000 + 5000));
            }
            ChangeTextView textView = (ChangeTextView) selectedTab.getChildAt(1);
            textView.setLevel((int) (positionOffset * 5000 + 5000));
        }

        if(!(tabIndex + 1 >= tabStripChildCount)){
            LinearLayout tab = (LinearLayout) getTabAt(tabIndex + 1);

            if(!tabLayoutState){
                ImageView img = (ImageView) tab.getChildAt(0);
                ((RevealDrawable)img.getDrawable()).setOrientation(RevealDrawable.VERTICAL);
                img.setImageLevel((int) (positionOffset * 5000));
            }
            ChangeTextView text = (ChangeTextView) tab.getChildAt(1);
            text.setLevel((int) (positionOffset * 5000)); 
        }

    }

水平方向滑动时图片,文字的动态变化部分:

final int tabStripChildCount = tabStrip.getChildCount();
            if (tabStripChildCount == 0 || page < 0 || page >= tabStripChildCount) {
                return;
            }

            LinearLayout selectedTab = (LinearLayout) getTabAt(page);
            ImageView imageView = (ImageView) selectedTab.getChildAt(0);
            ((RevealDrawable)imageView.getDrawable()).setOrientation(RevealDrawable.HORIZONTAL);
            if (0f < positionOffset && positionOffset <= 1f) {
                imageView.setImageLevel((int) ((1 - positionOffset) * 5000 + 5000));
            }

            for (int i = 0, size = tabStrip.getChildCount(); i < size; i++) {
                ChangeTextView textView = (ChangeTextView) ((LinearLayout) tabStrip.getChildAt(i)).getChildAt(1);
                if (0f < positionOffset && positionOffset <= 1f) {
                    textView.setAlpha((1 - positionOffset)); //文字渐变
                    if(positionOffset > 0.9f){ // 大于0.9时隐藏
                        textView.setVisibility(INVISIBLE);
                    }else{
                        textView.setVisibility(VISIBLE);
                    }
                }
            }

tabStrip.onViewPagerPageChanged(positionOffset);//控制指示器,背景,阴影变化。

到此位置,大体流程就完了。

5.一些小问题的解决

1.充满整个屏幕

如果TabView数量较少时,高度未能撑满整个屏幕时,显示效果是这样的。

这里写图片描述

这样看起来有点尴尬了。虽然我设置了高度为MATCH_PARENT但是没有起作用。

addView(tabStrip, LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);

很简单添加setFillViewport(true)即可。顾名思义,这个属性允许 ScrollView中的组件大小在不足时去充满它。

2.点击问题

看效果我们知道ChangeTabLayout在收起时,虽然文字已经隐藏掉了,但是它仍然消耗着手势操作。导致收起时,我们无法点击下方的 ViewPager,并且可以滑动ChangeTabLayout

我的解决方法是,计算文字部分的区域,进行判断是否拦截。

 @Override
    public boolean onTouchEvent(MotionEvent event) {
        if(tabLayoutState){
            return super.onTouchEvent(event);
        }else {
            final int action = event.getAction();
            switch (action) {
                case MotionEvent.ACTION_DOWN: //收起时点击不拦截,传入下层
                    return false;
                case MotionEvent.ACTION_MOVE: //收起时,滑动文字部分拦截
                    if(tabImageHeight + (int) (20 * density) < event.getRawX()){
                        return true;
                    }
                    break;
            }
            return super.onTouchEvent(event);
        }
    }

3.文字的显示异常

再点击ChangeTabLayout进行切换页面时,有时会导致如下异常显示。

这里写图片描述

原因通过排查后发现,我使用了viewPager.setCurrentItem(i)方法进行切换。导致ViewPager再切换中有一个平滑的滚动,监听方法onPageScrolled收到了部分页面的反馈数值。当然简单的解决方法是使用viewPager.setCurrentItem(i, false)进行切换。

然而倔强的我选择不将就(互相折磨到白头,悲伤坚决不放手~~)。想到了这样的解决办法。

在触摸ViewPager时将flag改为true。在点击切换时设置为false。每次变化前进行判断。

/**
 * tabView切换是否需要文字实时变化
 */
private boolean flag = false;

private class ViewPagerTouchListener implements OnTouchListener{

        @Override
        public boolean onTouch(View v, MotionEvent event) {
            switch (event.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    flag = true;
                    break;
            }
            return false;
        }
    }

 if(flag){
     ChangeTextView textView = (ChangeTextView) selectedTab.getChildAt(1);
     textView.setLevel((int) (positionOffset * 5000 + 5000));
  }

4.onPageScrolled监听不正常

在竖屏状态下水平滑动的ViewPageonPageScrolled监听不正常。

正常的打印是这样的:(滑动结束时 n页 – 0.0)

这里写图片描述

结果竖屏状态下会这样(两种):

这里写图片描述

这里写图片描述

这个异常打印有知道的望告知一下。感谢!

解决办法:

 public void setPageScrolled(int p, int position, float positionOffset) {
        // 统一异常数据
        if (positionOffset > 0.99 && positionOffset < 1){
            positionOffset = 0;
            position = position + 1;
        }else if (positionOffset < 0.01 && positionOffset > 0.00001){
            positionOffset = 0;
        }
 }

发布了已经有几天了,也收到了大家反馈的问题。在此非常感谢!突然觉得细还是大家细,我还是太粗了。。。趁着有时间整理了以上的实现思路,希望对感兴趣的你有帮助。

源码在此,多多点赞点星哦~~

版权声明:本文为博主原创文章,未经博主允许不得转载。http://blog.csdn.net/qq_17766199 举报

相关文章推荐

Java中如何判断一个集合中的一个元素不在另一个集合中?把不存在的元素移除

判断一个元素在集合中很容易,只需要遍历这个集合,然后与每个值进行比较,如果有相等的,那么就存在在集合中,然后反过来,就是不存在集合中的,找到它好像挺容易,但是写起代码来好像有点绕,那么,现在就把它写出...

Android属性动画完全解析(上),初识属性动画的基本用法

在手机上去实现一些动画效果算是件比较炫酷的事情,因此Android系统在一开始的时候就给我们提供了两种实现动画效果的方式,逐帧动画(frame-by-frame animation)和补间动画(twe...

我是如何成为一名python大咖的?

人生苦短,都说必须python,那么我分享下我是如何从小白成为Python资深开发者的吧。2014年我大学刚毕业..

Android快速开发系列 10个常用工具类

转载请标明出处:http://blog.csdn.net/lmj623565791/article/details/38965311,本文出自【张鸿洋的博客】打开大家手上的项目,基本都会有一大批的辅助...

Java爬虫,信息抓取的实现

今天公司有个x

Android DiskLruCache 源码解析 硬盘缓存的绝佳方案

转载请标明出处: http://blog.csdn.net/lmj623565791/article/details/47251585; 本文出自:【张鸿洋的博客】 一、概述依旧是整理东西...

Android BitmapShader 实战 实现圆形、圆角图片

转载请标明出处:http://blog.csdn.net/lmj623565791/article/details/41967509,本文出自:【张鸿洋的博客】1、概述记得初学那会写过一篇博客Andr...

Android UI性能优化 检测应用中的UI卡顿

本文已在我的公众号hongyangAndroid首发。 转载请标明出处: http://blog.csdn.net/lmj623565791/article/details/586263...

Android 自定义View (一)

很多的Android入门程序猿来说对于Android自定义View,可能都是比较恐惧的,但是这又是高手进阶的必经之路,所有准备在自定义View上面花一些功夫,多写一些文章。先总结下自定义View的步骤...

Android AutoLayout全新的适配方式 堪称适配终结者

转载请标明出处: http://blog.csdn.net/lmj623565791/article/details/49990941; 本文出自:【张鸿洋的博客】 一、概述相信Andro...
返回顶部
收藏助手
不良信息举报
您举报文章:深度学习:神经网络中的前向传播和反向传播算法推导
举报原因:
原因补充:

(最多只允许输入30个字)