打造炫酷通用的ViewPager指示器 - Adapter模式适配所有

1.概述



  上一期我们已经写了一篇 打造炫酷通用的ViewPager指示器 - 玩转字体变色 可是这种效果虽然绚烂可以装装A和C之间,但是在实际的大多数效果中并不常见,只是在内涵段子中有这个效果而已,那么这一期我们就用Adapter适配器模式适配所有的效果,堪称终结者。附视频地址:等这个周末吧

  

  

2.效果实现 



2.1 整合上一个实例:

  我还是还是拿上一个实例来做演示吧。这里我贴几种常见的效果,首先声明Android自带的有这个控件叫TabLayout,大家可以自己用用试试看好用不?我也用过但是不做任何评价,自己造的轮子还是想怎么用就怎么用。

  

  

  还有一些奇葩的效果如每个头部Item布局不一样,还有上面是图片下面是文字选中的效果各不相同等等,我们都要去适配。

  

2.2 实现思路:

  我在老早的时候用过ViewPageIndicator,还没毕业出来工作的时候,好不好用我也不做评价,就是那个时候搞了一晚上没搞出来第二天一看原来是activity的Theme主题没有配置,大家手上肯定也有类似的效果也都可以用,只是以个人的理解来自己造一个轮子。

  2.2.1 控件肯定是继承ScrollView因为可以左右滑动,如果再去自定义ViewGroup肯定不划算。

  2.2.2 怎样才能适合所有的效果,难道我们把所有可能出现的效果都写一遍吗?这的确不太可能,所以肯定采用Adapter适配器模式。

  2.2.3 我们先动起来从简单的入手,先做到动态的添加不同的布局条目再说吧。

  

2.3 自定义TrackIndicatorView动态添加布局:

  这里为了适配所有效果,所以决定采用适配器Adapter设计模式,上面也提到过。至于什么是适配器模式大家需要看一下这个 Android设计模式源码解析之适配器(Adapter)模式 这是理论篇,但是仔细看过我博客的哥们应该知道我其实 Adapter设计模式理论与实践相结合写过很多效果和框架了。这里不做过多的讲解,写着写着看着看着就会了就理解了。

  2.3.1 我们再也不能直接传字符串数组或是传对象数组过去让自定义View去处理了,所以我们先确定一个自定义的Adapter类,getCount() 和 getView(int position,ViewGroup parent) 先用这两个方法吧后面想到了再说。

  

/**
 * Created by Darren on 2016/12/7.
 * Email: [email protected]
 * Description:  指示器的适配器
 */
public abstract class IndicatorBaseAdapter{
    // 获取总的条数
    public abstract int getCount();

    // 根据当前的位置获取View
    public abstract View getView(int position,ViewGroup parent);
}

  2.3.2 然后我们来实现指示器的自定义View,TrackIndicatorView 继承自 HorizontalScrollView 。然后我们利用传递过来的Adapter再去动态的添加,我这里就直接上代码吧  

/**
 * Created by Darren on 2016/12/13.
 * Email: [email protected]
 * Description: ViewPager指示器
 */

public class TrackIndicatorView extends HorizontalScrollView {

    // 自定义适配器
    private IndicatorBaseAdapter mAdapter;

    // Item的容器因为ScrollView只允许加入一个孩子
    private LinearLayout mIndicatorContainer;

    public TestIndicator(Context context) {
        this(context, null);
    }

    public TestIndicator(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public TestIndicator(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        // 初始化Indicator容器用来存放item
        mIndicatorContainer = new LinearLayout(context);
        addView(mIndicatorContainer);
    }

    public void setAdapter(IndicatorBaseAdapter adapter) {
        if (adapter == null) {
            throw new NullPointerException("Adapter cannot be null!");
        }
        this.mAdapter = adapter;

        // 获取Item个数
        int count = mAdapter.getCount();

        // 动态添加到布局容器
        for (int i = 0; i < count; i++) {
            View indicatorView = mAdapter.getView(i, mIndicatorContainer);
            mIndicatorContainer.addView(indicatorView);
        }
    }
}

  效果可想而知,可以写一个Activity测试一下,目前可以动态的添加多个不同样式的布局,如果超出一个屏幕可以左右滑动,我这里就不做演示,待会一起吧。

  

  2.3.3 动态的制定指示器Item的宽度:

  

  目前我们虽然能够动态的去添加各种布局,但是Item的宽度是任意的,我们需要在布局文件中指定一屏显示多少个,如果没有指定那么就获取Item中最宽的一个,如果不够一屏显示就默认显示一屏。我们需要使用自定义属性,这里就不做过多的讲,实在不行大家就自己去看看有关自定义属性的博客或是直接google搜索一下。

    // 获取一屏显示多少个Item,默认是0
    private int mTabVisibleNums = 0;

    // 每个Item的宽度
    private int mItemWidth = 0;

    public TrackIndicatorView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        // 之前代码省略...
        // 获取自定义属性值 一屏显示多少个
        TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.TrackIndicatorView);
        mTabVisibleNums = array.getInt(R.styleable.TrackIndicatorView_tabVisibleNums,
            mTabVisibleNums);
        array.recycle();
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        super.onLayout(changed, l, t, r, b);
        if (changed) {
            // 指定Item的宽度
            mItemWidth = getItemWidth();
            int itemCounts = mAdapter.getCount();
            for (int i = 0; i < itemCounts; i++) {
                // 指定每个Item的宽度
                mIndicatorContainer.getChildAt(i).getLayoutParams().width = mItemWidth;
            }
            Log.e(TAG, "mItemWidth -> " + mItemWidth);
        }
    }

    /**
     * 获取每一个条目的宽度
     */
    public int getItemWidth() {
        int itemWidth = 0;
        // 获取当前控件的宽度
        int width = getWidth();
        if (mTabVisibleNums != 0) {
            // 在布局文件中指定一屏幕显示多少个
            itemWidth = width / mTabVisibleNums;
            return itemWidth;
        }
        // 如果没有指定获取最宽的一个作为ItemWidth
        int maxItemWidth = 0;
        int mItemCounts = mAdapter.getCount();
        // 总的宽度
        int allWidth = 0;

        for (int i = 0; i < mItemCounts; i++) {
            View itemView = mIndicatorContainer.getChildAt(i);
            int childWidth = itemView.getMeasuredWidth();
            maxItemWidth = Math.max(maxItemWidth, childWidth);
            allWidth += childWidth;
        }

        itemWidth = maxItemWidth;

        // 如果不足一个屏那么宽度就为  width/mItemCounts
        if (allWidth < width) {
            itemWidth = width / mItemCounts;
        }
        return itemWidth;
    }

  目前我们各种情况都测试了一下,一种是直接在布局文件中指定一屏可见显示4个,一种是不指定就默认以最大的Item的宽度为准,最后一种就是不指定又不足一个屏幕默认就显示一屏。看一下效果吧

2.4结合ViewPager

  

  接下来我们就需要结合ViewPager了,也就需要实现一系列重要的效果:

  2.4.1. 当ViewPager滚动的时候头部需要自动将当前Item滚动到最中心;

  2.4.2. 点击Item之后ViewPager能够切换到对应的页面;

  2.4.3. 需要页面切换之后需要回调,让用户切换当前选中的状态,需要在Adapter中增加方法;

  2.4.4. 有些效果需要加入指示器,但并不是每种效果都需要

2.4.1. 当ViewPager滚动的时候头部自动将当前Item滚动到最中心

  我们目前不光需要Adapter,还需要一个参数就是ViewPager,需要监听ViewPager的滚动事件

    /**
     * 重载一个setAdapter的方法
     * @param adapter  适配器
     * @param viewPager  联动的ViewPager
     */
    public void setAdapter(IndicatorBaseAdapter adapter, ViewPager viewPager) {
        // 直接调用重载方法
        setAdapter(adapter);

        // 为ViewPager添加滚动监听事件
        this.mViewPager = viewPager;
        mViewPager.addOnPageChangeListener(this);
    }

    @Override
    public void onPageScrolled(int position, float positionOffset,
        int positionOffsetPixels) {
        // 在ViewPager滚动的时候会不断的调用该方法
        Log.e(TAG,"position --> "+position+"  positionOffset --> "+positionOffset);
        // 在不断滚动的时候让头部的当前Item一直保持在最中心
        indicatorScrollTo(position,positionOffset);
    }

    /**
     * 不断的滚动头部
     */
    private void indicatorScrollTo(int position, float positionOffset) {
        // 当前的偏移量
        int currentOffset = (int) ((position + positionOffset) * mItemWidth);
        // 原始的左边的偏移量
        int originLeftOffset = (getWidth()-mItemWidth)/2;
        // 当前应该滚动的位置
        int scrollToOffset = currentOffset - originLeftOffset;
        // 调用ScrollView的scrollTo方法
        scrollTo(scrollToOffset,0);
    }

  目前我们滚动ViewPager的时候,当前指示器条目会一直保持在最中心,activity的代码我就没贴出来了,这个待会可以下载我的源码看看。我们看看效果

  

  2.4.2. 点击Item之后ViewPager能够切换到对应的页面

    public void setAdapter(IndicatorBaseAdapter adapter) {
        if (adapter == null) {
            throw new NullPointerException("Adapter cannot be null!");
        }
        this.mAdapter = adapter;

        // 获取Item个数
        int count = mAdapter.getCount();

        // 动态添加到布局容器
        for (int i = 0; i < count; i++) {
            View indicatorView = mAdapter.getView(i, mIndicatorContainer);
            mIndicatorContainer.addView(indicatorView);
            switchIndicatorClick(indicatorView,i);
        }
    }

    /**
     * Indicator条目点击对应切换ViewPager
     */
    private void switchIndicatorClick(View indicatorView, final int position) {
        indicatorView.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                if(mViewPager != null){
                    // 对应切换ViewPager
                    mViewPager.setCurrentItem(position);
                }
                // IndicatorItem对应滚动到最中心
                indicatorSmoothScrollTo(position);
            }
        });
    }

    /**
     * 滚动到当前的位置带动画
     */
    private void indicatorSmoothScrollTo(int position) {
        // 当前的偏移量
        int currentOffset =  ((position) * mItemWidth);
        // 原始的左边的偏移量
        int originLeftOffset = (getWidth()-mItemWidth)/2;
        // 当前应该滚动的位置
        int scrollToOffset = currentOffset - originLeftOffset;
        // smoothScrollTo
        smoothScrollTo(scrollToOffset,0);
    }

  我们运行起来之后会发现一个问题,我们点击会切换对应的ViewPager但是这个时候还是会调用onPageScrolled()方法,这个就比较dan疼,所以我们必须解决,如果是点击我就不让其执行onPageScrolled()里面的代码。

  

  2.4.3. 需要页面切换之后需要回调,让用户切换当前选中的状态,需要在Adapter中增加方法

在Adapter中增加两个回调方法,一个是高亮当前选中方法highLightIndicator(View view) ,恢复默认方法restoreIndicator(View view),这两个方法可以不用写成抽象的,为了方便我们干脆使用泛型

/**
 * Created by Darren on 2016/12/7.
 * Email: 240336124@qq.com
 * Description:  指示器的适配器
 */
public abstract class IndicatorBaseAdapter<Q extends View>{
    // 获取总的条数
    public abstract int getCount();

    // 根据当前的位置获取View
    public abstract Q getView(int position, ViewGroup parent);

    // 高亮当前位置
    public void highLightIndicator(Q indicatorView){

    }

    // 重置当前位置
    public void restoreIndicator(Q indicatorView){

    }
}

TrackIndicatorView

    @Override
    public void onPageSelected(int position) {
        // 重置上一个位置的状态
        View lastView = mIndicatorContainer.getChildAt(mCurrentPosition);
        mAdapter.restoreIndicator(lastView);
        // 高亮当前位置的状态
        mCurrentPosition = position;
        highLightIndicator(mCurrentPosition);
    }

    /**
     * 高亮当前位置
     */
    private void highLightIndicator(int position) {
        View currentView = mIndicatorContainer.getChildAt(position);
        mAdapter.highLightIndicator(currentView);
    }

  一步两步一步两步总算是快到头了,接下来我们只需要加入指示器就可以了,当前这里面涉及到属性动画,如果不是很了解那就去看一下我的视频或者去google官网看一下吧。

  

  2.4.4. 有些效果需要加入指示器,但并不是每种效果都需要

  

/**
 * Created by Darren on 2016/12/7.
 * Email: [email protected]
 * Description:  指示器的容器包括下标
 */

public class IndicatorContainer extends RelativeLayout {
    private LinearLayout mIndicatorContainer;
    private Context mContext;
    // 底部跟踪的View
    private View mBottomTrackView;
    private String TAG = "IndicatorContainer";
    // 距离左边的初始距离
    private int mInitLeftMargin = 0;
    private RelativeLayout.LayoutParams mBottomTrackParams;
    private int mTabWidth;

    public IndicatorContainer(Context context) {
        this(context, null);
    }

    public IndicatorContainer(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public IndicatorContainer(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        this.mContext = context;
    }

    @Override
    public void addView(View child) {
        if (mIndicatorContainer == null) {
            // 初始化容器
            mIndicatorContainer = new LinearLayout(mContext);
            RelativeLayout.LayoutParams params = new LayoutParams(
                    ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
            super.addView(mIndicatorContainer, params);
        }
        mIndicatorContainer.addView(child);
    }

    public int getIndicatorCount() {
        return mIndicatorContainer.getChildCount();
    }

    public View getIndicatorAt(int index) {
        return mIndicatorContainer.getChildAt(index);
    }

    /**
     * 添加底部跟踪指示器
     * @param bottomTrackView
     */
    public void addBottomTrackView(View bottomTrackView) {
        if (bottomTrackView == null) return;
        mBottomTrackView = bottomTrackView;
        super.addView(mBottomTrackView);

        // 指定一个规则添加到底部
        mBottomTrackParams = (LayoutParams) mBottomTrackView.getLayoutParams();
        mBottomTrackParams.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM);

        // 计算和指定指示器的宽度
        int width = mBottomTrackParams.width;
        mTabWidth = mIndicatorContainer.getChildAt(0).getLayoutParams().width;
        if (width == ViewGroup.LayoutParams.MATCH_PARENT) {
            width = mTabWidth;
        }
        // 计算跟踪的View初始左边距离
        if (width < mTabWidth) {
            mInitLeftMargin = (mTabWidth - width) / 2;
        }
        mBottomTrackParams.leftMargin = mInitLeftMargin;
    }

    /**
     * 底部指示器移动到当前位置
     */
    public void bottomTrackScrollTo(int position, float offset) {
        if (mBottomTrackView == null) return;
        // Log.e(TAG,"position --> "+position+" offset --> "+offset);
        mBottomTrackParams.leftMargin = (int) (mInitLeftMargin + (position + offset) * mTabWidth);
        mBottomTrackView.setLayoutParams(mBottomTrackParams);
    }

    /**
     * 开启一个动画移动到当前位置
     */
    public void smoothScrollToPosition(int position) {
        if (mBottomTrackView == null) return;
        // 获取当前指示器距左边的距离
        final int mCurrentLeftMargin = mBottomTrackParams.leftMargin;
        // 计算出最终的距离
        final int finalLeftMargin = mTabWidth * position + mInitLeftMargin;
        // 用于动画执行的事件
        final int distance = finalLeftMargin - mCurrentLeftMargin;
        // 利用属性动画不断的更新距离
        ObjectAnimator animator = ObjectAnimator.ofFloat(mBottomTrackView, "leftMargin",
                mCurrentLeftMargin, finalLeftMargin).setDuration(Math.abs(distance));
        animator.setInterpolator(new DecelerateInterpolator());
        animator.start();
        // 添加动画监听不断的更新 leftMargin
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                float currentLeftMargin = (float) animation.getAnimatedValue();
                // Log.e(TAG, "current --> " + currentLeftMargin);
                setBottomTrackLeftMargin((int) currentLeftMargin);
            }
        });
    }

    /**
     * 设置底部跟踪指示器的左边距离
     */
    public void setBottomTrackLeftMargin(int bottomTrackLeftMargin) {
        mBottomTrackParams.leftMargin = bottomTrackLeftMargin;
        mBottomTrackView.setLayoutParams(mBottomTrackParams);
    }
}

 最后我们看看一些奇葩的一些需求,这是录制的效果,最后附视频地址:等这周末吧。

 

 

时间: 2024-08-02 02:46:10

打造炫酷通用的ViewPager指示器 - Adapter模式适配所有的相关文章

Android 自定义控件玩转字体变色 打造炫酷ViewPager指示器

Android 自定义控件玩转字体变色 打造炫酷ViewPager指示器

Android 教你打造炫酷的ViewPagerIndicator 不仅仅是高仿MIUI

1.概述 哈,今天给大家带来一个ViewPagerIndicator的制作,相信大家在做tabIndicator的时候,大多数人都用过 TabPageIndicator,并且很多知名APP都使用过这个开源的指示器.大家有没有想过如何自己去实现这样的一个指示器,并且代码会有多复杂 呢~~~今天,我就带领大家来从无到有的实现这样一个指示器,当然了,不准备一模一样,搞得没有创新似的,再看标题,跟MIUI相关,所以我们准备做一个 特性与TabPageIndicator一致的,但是样子和MIUI的Tab一

JParticles 2.0 发布,打造炫酷的粒子特效

JParticles 2.0 发布,打造炫酷的粒子特效.不好意思哈,在这么繁花似锦的世界里,标题不得不取得吸引眼球一点哈,不然...还是不啰嗦了,我们进入正题吧 简单介绍一下 JParticles 2.0 版本之前还叫 Particleground.js,相信在用的朋友应该不会陌生,关于 1.x 版本的宣传文案可以移步看这里哈,或许可以帮助你了解 JParticles 2.0 的一些东西. 我们一贯的理念 我们(我/笑哭)一贯的理念是信仰:"The Write Less, Do More&quo

打造Android 最实用的ViewPager 指示器控件

为什么我说它是最实用的 ViewPager 指示器控件呢? 它有以下几个特点: 1.通过自定义 View 来实现,代码简单易懂: 2.使用起来非常方便: 3.通用性高,大部分涉及到 ViewPager 指示器的地方都能使用此控件: 4.实现了两种指示器效果(具体请看效果图) 一.先来看效果图 传统版指示器的效果图: 流行版指示器的效果 二.分析 如果单纯的要实现此功能,相信,大家都能实现,而我也不会拿出来这里讲了,这里我是要把它打造成一个控件,通俗一点讲就是,在以后可以直接拿来用,而不需要修改代

Photoshop和WPF双剑配合,打造炫酷个性的进度条控件

现在如果想打造一款专业的App,UI的设计和操作的简便性相当重要.UI设计可以借助Photoshop或者AI等设计工具,之前了解到WPF设计工具Expression Blend可以直接导入PSD文件或者AI设计文件(当然不是全部特征支持),最近研究了一下,也废了一番周折,好在最后实现了预期的效果.下面将step by step用示例说明如何先用PS构建一个矢量图形模板,然后用Expression Blend导入PSD文件,并获取PATH的Data值,为打造一款炫酷的个性进度条控件构建美观UI.

Linux进阶之使用Oh-My-Zsh打造炫酷终端

Oh My Zsh是基于zsh命令行的一个扩展工具集,提供了丰富的扩展功能.除了功能增强之外,还提供非常丰富的主题.使用Oh-My-Zsh打造酷炫Shell终端的步骤(Deepin系统): 原始终端: 第一步:下载并安装zsh [email protected]:~/Desktop$ sudo apt install zsh          #需要输入密码 第二步:下载并安装git [email protected]:~/Desktop$ sudo apt install git 第三步:下载

GJM :JS + CSS3 打造炫酷3D相册

中秋主题的3D旋转相册 如图,这是通过Javascript和css3来实现的.整个案例只有不到80行代码,我希望通过这个案例,让正处于迷茫期的js初学者感受到学习的乐趣.我会尽可能讲得详细,不需要你对css和js有多么高深的理解,你也可以跟着一步步做出来.如果你是为了讨女票开心,那么也完全可以把图片换成对方的照片,在某个特别的时刻给对方一个惊喜哦 ~ css3的强大使得网页的展示变得空前得丰富起来,再配合简单的js代码,就可以实现这个效果.好了,话不多说,让我们开始吧. 1. 页面模板 <!do

DreamSceneSeven打造炫酷的动态桌面

动态桌面配置图文教程 --------------------------------------------Start Now-------------------------------------------- 一.前言 1.我们的桌面 经典的桌面是WIN7 WIN8或这个WIN10的原生桌面,一张静态的带着微软LOGO的静态图片.当然我们很少会用原生桌面,因为太普通,周围的朋友几乎人手一本笔记本,桌面没有一个相同的.这在哪应该都是很平常的现象. 说起动态桌面,有种图片后缀名为gif(常见

Android_字体变色,viewpager指示器

简介 本篇是来自鸿洋_大神的Android 自定义控件玩转字体变色 打造炫酷ViewPager指示器这篇博客,刚开始看时觉得不是很好理解,现在自己减去了他的一些代码,自己写的一个,也实现了同样的效果,感觉好理解多了. 先看一下实现后的效果: 要实现这样的文字跟随viewpager的滑动而逐渐改变颜色的效果,这里很明显底色字一直在的,所以我们要考虑的主要是红色字体的绘制.绘制红色字体用的是canvas的clipRect()方法.根据要改变的宽度系数,切割要绘制的位置.这里运用了自定义属性,方便我们