ViewPager3D库使用

阅读原文

ViewPager3D库使用

无意间发现了某大神封装的ViewPager3D库,一时手痒,就跑去down下来研究研究,附上作者原文及Demo下载地址

作者原文地址:

http://www.jianshu.com/p/4edb837a570f

ViewPager3D库及sample下载地址:

https://github.com/bambootang/ViewPager3D


先上两张图感受一下气氛

image
image

把这一块整明白确实是花费了不少时间,起初没整明白平面图是怎么画出来的,后来用AE和C4D简单做了个动画模拟,得到了一些参数和思路,大体上还算能理解!


01 - ActivityManager

首先采用ActivityManager类来管理Activity,该类使用栈结构作为Activity的栈管理

/**
 * @author: Hashub
 * @WeChat: NGSHMVP
 * @Date: 2018/10/5 11:14
 * @function:统一应用程序中所有的Activity的栈管理(单例) 涉及到activity的添加、删除指定、删除当前、删除所有、返回栈大小的方法
 */
public class ActivityManager
{
    //提供栈的对象
    private Stack activityStack = new Stack<>();
    private static ActivityManager activityManager = new ActivityManager();

    //单例模式:饿汉式,,私有化构造器
    private ActivityManager()
    {
    }

    /**
     * 提供获取单例实例的方法
     *
     * @return
     */
    public static ActivityManager getInstance()
    {
        return activityManager;
    }

    /**
     * activity的添加
     *
     * @param activity
     */
    public void add(Activity activity)
    {
        if (activity != null)
        {
            activityStack.add(activity);
        }
    }

    /**
     * 移除除指定的activity
     *
     * @param activity
     */
    public void remove(Activity activity)
    {
        if (activity != null)
        {
            for (int i = activityStack.size(); i >= 0; i--)
            {
                Activity currentActivity = activityStack.get(i);
                if (currentActivity.getClass().equals(activity.getClass()))//移除所有同类的activity对象
                {
                    currentActivity.finish();//销毁当前的activity
                    activityStack.remove(i);//从栈空间移除
                }
            }
        }
    }

    /**
     * 移除当前的activity
     */
    public void removeCurrentActivity()
    {
//方式一:
//        Activity activity = activityStack.get(activityStack.size() - 1);
//        activity.finish();
//        activityStack.remove(activityStack.size() - 1);
//方式二:
        Activity activity = activityStack.lastElement();
        activity.finish();
        activityStack.remove(activity);
    }

    /**
     * 移除所有的activity
     */
    public void removeAll()
    {
        for (int i = activityStack.size(); i >= 0; i--)
        {
            Activity activity = activityStack.get(i);
            activity.finish();
            activityStack.remove(activity);
        }
    }

    /**
     * 返回栈大小
     *
     * @return
     */
    public int size()
    {
        return activityStack.size();
    }
}

在一般情况下,见得更多的是使用ArrayList来管理Activity,Stack继承于Vector,而Vector又是List接口的实现类之一。

Vector 是一个古老的集合,JDK1.0就有了。大多数操作与ArrayList相同,区别之处在于Vector是线程安全的。
在各种list中,最好把ArrayList作为缺省选择。当插入、删除频繁时,使用LinkedList;Vector总是比ArrayList慢,所以尽量避免使用。

ArrayList 是 List 接口的典型实现类
本质上,ArrayList是对象引用的一个变长数组,
ArrayList 是线程不安全的,而 Vector 是线程安全的,即使为保证 List 集合线程安全,也不推荐使用Vector。

Vector新增方法:

void addElement(Object obj)

void insertElementAt(Object obj,int index)

void setElementAt(Object obj,int index)

void removeElement(Object obj)

void removeAllElements()

Stack(Last In First Out)

翻译成人话就是“吃多了吐”!堆栈是限制在表的一端进行插入和删除的线性表,简称为栈。栈通常情况下又分为两种,顺序栈和链栈。使用Stack作为Activity栈管理目的是为了回顾一下数据结构。

顺序栈

利用顺序储存方式实现的栈称为顺序栈。类似于顺序表的定义,栈中的数据元素用一个足够大的一维数组DataType data[MAXSIZE]来实现,栈底位置可以设置在数组的任何一个端点,而栈顶是随着插入和删除而变化的,因此需要一个栈顶位置的标识。

链栈

用链式存储结构实现的栈称为链栈。通常链栈用单链表来表示,因此其结点结构与单链表的结构相同。

线性表

在一个线性表中数据元素的类型是相同的,或者说线性表是由同一类型的数据元素构成的线性结构。

线性结构

线性结构的特点是数据元素间是一种线性关系,数据元素“一个接着一个的排列”。在线性结构中,有且仅有一个元素被称为“第一个”,除第一个元素外其他元素均有唯一一个“前驱”;有且仅有一个元素被称为“最后一个”,除最后一个元素之外其他元素均有唯一一个“后继”。

顺序表

基本的线性表有两种储存方式:顺序存储和链式存储,线性表的顺序存储是指在内存中用地址连续的一块存储空间顺序存放线性表的各元素,用这种存储形式存储的线性表称为顺序表。

线性表的链式存储

由于顺序表的存储特点是用物理上的相邻实现了逻辑上的相邻,它要求用来连续的存储单元顺序存储线性表中的各元素。因此,对于插入、删除时需要通过移动数据元素来实现,影响了效率。而链式存储结构不需要用地址连续的存储单元来实现,因为它不要求逻辑上相邻的两个数据元素物理上也相邻。链式存储结构一般有3种实现方式:单链表、循环链表、双向链表。


02 - BaseActivity

抽取了一个Activity的基类,以免写更多的重复性代码,也是为了代码看起来简洁些

/**
 * @author: Hashub
 * @WeChat: NGSHMVP
 * @Date: 2018/10/5 16:25
 * @function:Activity抽取
 */
public abstract class BaseActivity extends Activity
{
    @Override
    public void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(getLayoutId());
        ButterKnife.bind(this);

        //将当前的activity添加到ActivityManager中
        ActivityManager.getInstance().add(this);
        initData();
    }

    @Override
    public void onWindowFocusChanged(boolean hasFocus)
    {
        super.onWindowFocusChanged(hasFocus);
    }

    /**
     * 暴露给子类的抽象方法
     */
    protected abstract void initData();

    protected abstract int getLayoutId();


    /**
     * 启动新的activity
     *
     * @param Activity
     * @param bundle
     */
    public void goToActivity(Class Activity, Bundle bundle)
    {
        Intent intent = new Intent(this, Activity);
        //携带数据
        if (bundle != null && bundle.size() != 0)
        {
            intent.putExtra("data", bundle);
        }
        startActivity(intent);
    }

    /**
     * 销毁当前的Activity
     *
     * @param
     */
    public void removeCurrentActivity()
    {
        ActivityManager.getInstance().removeCurrentActivity();
    }

    /**
     * 销毁所有的activity
     */
    public void removeAllActivity()
    {
        ActivityManager.getInstance().removeAll();
    }

    @Override
    protected void onDestroy()
    {
        super.onDestroy();
        ButterKnife.unbind(this);
    }
}

这里使用ButterKnife实例化控件,因为懒!ButterKnife固然是省事,但在某些情况下也会出现问题,虽然不影响使用,但是会报一些莫名奇妙的错误,当然,该老老实实使用“Fbi”的时候还是不要偷懒......那是不可能的!

初始化控件脚本网址:复制粘贴xml布局文件就能得到你想要的!

https://www.buzzingandroid.com/tools/android-layout-finder/

3 - UseViewPagerActivity

这里摘取了其中一种效果,Sample内置的其他效果操作类似

public class UseViewPagerActivity extends BaseActivity{}

在附带的案例中,使用的是本地图片测试,这里同样用本地图片,但是比较特别的是这里采用了HashMap来作为管理图片而不是常见的ArrayList。

HashMap是 Map 接口使用频率最高的实现类。
允许使用null键和null值,与HashSet一样,不保证映射的顺序。

HashMap 判断两个 key 相
等的标准是:两个 key 通过 equals() 方法返回 true,hashCode 值也相等。

HashMap 判断两个 value相等的标准是:两个 value 通过 equals() 方法返回 true。

//准备图片数据
    int[] imgIds = {R.drawable.img001,
            R.drawable.img002,
            R.drawable.img003,
            R.drawable.img004,
            R.drawable.img005,
            R.drawable.img006,
            R.drawable.img007,
            R.drawable.img008,
            R.drawable.img009,
            R.drawable.img010
    };

    HashMap imageViewList = new HashMap<>();

04 - 添加小红点

类似于广告轮播图的效果,实际上小红点是一个现象布局,里面填充ImageView,再用Shape图形填充ImageView,在Android中,可以通过来自定义图形。

    /**
     * 添加红点
     *
     * @param i
     */
    private void addPoint()
    {

        for (int i = 0; i < imgIds.length; i++)
        {
            //添加点
            ImageView point = new ImageView(this);
            point.setBackgroundResource(R.drawable.point_selector);
            LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(8, 8);

            if (i == 0)
            {
                //显示红色
                point.setEnabled(true);

            }
            else
            {
                //显示灰色
                point.setEnabled(false);
                params.leftMargin = 0;
            }

            point.setLayoutParams(params);
            ll_point_group.addView(point);
        }

    }

这里LinearLayout.LayoutParams(8, 8);没有做像素匹配,这个用工具类转换一下就好了。

point_selector

选中的时候,高亮为红色,没有选中为灰色



    
    


point_normal && point_press

它们只是颜色不同



    
    

 

05 - 设置监听ViewPager页面的改变

让小红点跟着图片滑动而改变颜色和位置,这里直接是设置了setOnPageChangeListener监听,虽然是个过时的方法,先用用再说!该方法中设置ViewPager页面的改变监听,主要重写onPageSelected(int position)方法

        /**
         * 当某个页面被选中了的时候回调
         *
         * @param position 被选中页面的位置
         */
        @Override
        public void onPageSelected(int position)
        {
            int realPosition = position % imgIds.length;

            //把上一个高亮的设置默认-灰色
            ll_point_group.getChildAt(prePosition).setEnabled(false);

            //当前的设置为高亮-红色
            ll_point_group.getChildAt(realPosition).setEnabled(true);

            prePosition = realPosition;
        }

realPosition是position取模运算的结果,这里对ViewPager做了无限循环的效果,所以需要该操作,轮播效果用哪个Handler处理就可以了,这里也没有做!

定义一个变量prePosition记录上一次高亮显示的位置

// 上一次高亮显示的位置
private int prePosition = 0;

06 - 设置ViewPager的适配器

    //设置适配器
    vpPagers.setAdapter(pagerAdapter);

MyBaseAdapter

这里对适配器也做了一下抽取,只是简单的抽取出同样的方法,把不一样的方法暴露给子类

/**
 * @author: Hashub
 * @WeChat: NGSHMVP
 * @Date: 2018/10/5 16:43
 * @function:
 */
public abstract class MyBaseAdapter extends PagerAdapter
{

    @Override
    public int getCount()
    {
        return Integer.MAX_VALUE;
    }

    @Override
    public boolean isViewFromObject(View view, Object object)
    {
        return view == object;
    }

    @Override
    public void destroyItem(ViewGroup container, int position, Object object)
    {
        container.removeView((View) object);
    }

    @Override
    public void setPrimaryItem(ViewGroup container, int position, Object object)
    {
        super.setPrimaryItem(container, position, object);
    }

    /**
     * 暴露给子类的方法
     *
     * @param container
     * @param position
     * @return
     */
    public abstract Object instantiateItem(ViewGroup container, int position);
}

ViewPager无限循环

ViewPager的工作机制,严格意义上就是PagerAdapter的执行顺序。

PagerAdapter作为ViewPager的适配器,无论ViewPager有多少页,PagerAdapter在初始化时也只初始化开始的2个View,即调用2次instantiateItem方法。而接下来每当ViewPager滑动时,PagerAdapter都会调用destroyItem方法将距离该页2个步幅以上的那个View销毁,以此保证PagerAdapter最多只管辖3个View,且当前View是3个中的中间一个,如果当前View缺少两边的View,那么就instantiateItem,如里有超过2个步幅的就destroyItem。

首先pageradapter初始化的时候会加载0和1(这里说的加载就是调用instantiateitem方法),向右滑动就会加载2,然后加载3,同时把0销毁(这里说的销毁就是调用destroyItem方法),以此类推,这里就是viewpager的缓存情况,默认就是缓存当前view的左右两个。不过这个值可以设置,比如设置成2,那么同时就会有2X2+1,5个view同时存在了,看自己情况设置。

设置getCount无限大

adapter中设置getCount为无限大,比如Integer.MAX_VALUE,这个值直接关系到ViewPager的“边界”,因此当我们把它设置为Integer.MAX_VALUE之后,用户基本就看不到这个边界了这个有下面两种情况:

第一种是初始化第一个位置为一个中间值(一般都是默认为0的),这个可以任意

第二种就是在instantiateItem()方法中对position进行取模操作。即

 @Override
        public Object instantiateItem(ViewGroup container, int position)
        {
            int realPosition = position % imgIds.length;
////
            ClipView clipView;
            if (imageViewList.containsKey(realPosition))
            {
                clipView = imageViewList.get(realPosition);
            }
            else
            {
                //ImageView imageView = imageViews.get(realPosition);
                ImageView imageView = new ImageView(container.getContext());

                imageView.setImageResource(imgIds[realPosition]);
                imageView.setAdjustViewBounds(false);
                imageView.setScaleType(ImageView.ScaleType.FIT_XY);

                clipView = new ClipView(container.getContext());
                clipView.setId(realPosition + 1);
                clipView.addView(imageView);
                imageViewList.put(realPosition, clipView);
            }
            container.addView(clipView);
            clipView.setOnClickListener(new View.OnClickListener()
            {
                @Override
                public void onClick(View v)
                {
                    Toast.makeText(v.getContext(), "v.getId " + v.getId(), Toast.LENGTH_SHORT).show();
                }
            });
            return clipView;

instantiateItem() 方法position的处理:

由于我们设置了count为 Integer.MAX_VALUE,因此这个position的取值范围很大很大,但我们实际要显示的内容肯定没这么多(往往只有几项),所以这里肯定会有求模操作。但是,简单的求模会出现问题:考虑用户向左滑的情形,则position可能会出现负值。所以我们需要对负值再处理一次,使其落在正确的区间内。这里测试的时候正常故不作处理。

instantiateItem() 方法父组件的处理:

通常我们会直接addView,但如果直接这样写,则肯能会抛出IllegalStateException。

假设一共有三个view,则当用户滑到第四个的时候就会触发这个异常,原因是我们试图把一个有父组件的View添加到另一个组件。但是,如果直接写成下面这样:

(ViewGroup)view.getParent().removeView(view);

则又会因为一开始的时候组件并没有父组件而抛出NullPointerException。因此,在addView之前需要进行一次判断。大概就是这个样子

2018-10-06_165110.png

07 - Viewpage的.setCurrentItem 导致UI线程的执行阻塞问题

在instantiateItem() 方法中设置clipView的点击事件,能够正常获取到position并且在ViewPager不循环(即getCount返回正常值)的情况下,setCurrentItem方法是有效的,但是在设置了无限循环后调用该方法就会造成UI线程的执行阻塞导致程序ANR。

参考了网上的一些方案,查看ViewPager源码,发现最终调用到这个方法:

void setCurrentItemInternal(int item, boolean smoothScroll, boolean always, int velocity) {}

方法里面有行判断:

if (mFirstLayout) {
// We don't have any idea how big we are yet and shouldn't have any pages either.
// Just set things up and let the pending layout handle things.
mCurItem = item;
if (dispatchSelected) {
dispatchOnPageSelected(item);
}
requestLayout();
} else {
populate(item);
scrollToItem(item, smoothScroll, velocity, dispatchSelected);
}

mFirstLayout是一个私有变量,默认为true,第一次设置数据时,mFirstLayout为true,然后在viewpager的
onLayout方法中就被设置成了false

当下次再次setCurrentItem时就进入了else中,这时,else中的代码就会引起UI卡顿。

该方案利用反射,强行修改mFirstLayout的值为true,经测试不管用

try {
Field mFirstLayout = ViewPager.class.getDeclaredField("mFirstLayout");
mFirstLayout.setAccessible(true);
mFirstLayout.set(vp_top, true);
myPagerAdapter.notifyDataSetChanged();
vp_top.setCurrentItem(Integer.MAX_VALUE/4-( Integer.MAX_VALUE/4 % ids_list_data.size() ));
}catch(Exception e) {
e.printStackTrace();
}

方案二:虽然不管用,但还是记录下来了。

1,先把这个view从父view中移除掉

ll_main.removeviewAt(1);

2, 重新设置一下adapter,并同时设置选中的一页

viewpager.setAdapter(adapter);
viewpager.setCurrentItem(Integer.MaxValue/2);

3, 重新添加回去

ll_main.addView(viewpager);

方案三:执行在Activity的onCreate方法中

这种方法显然是可行的,因为在初始化的时候,就设置了中间位置,但这与添加点击事件才执行setCurrentItem又有些不同,但是时间匆忙来不及做测试,先记录下来

 //设置中间位置,//要保证imageViews的整数倍
        //设置中间位置,//要保证imageViews的整数倍
        int item = Integer.MAX_VALUE / 2 - Integer.MAX_VALUE / 2 % imgIds.length;
        vpPagers.setCurrentItem(item);

08 - ClipView是个啥

public class ClipView extends LinearLayout implements Clipable{}

显然它继承了LinearLayout,而LinearLayout继承于ViewGroup,而ViewGroup又继承于View,其实是一个自定义的ViewGroup。

(1)dispatchDraw()

该方法用于对子视图进行遍历然后分别让子视图分别draw,方法内部会首先处理布局动画(也就是说布局动画是在这里处理的),如果有布局动画则会为每个子视图产生一个绘制时间,之后再有一个for循环对子视图进行遍历,来调用子视图的draw方法(实际为下边的drawChild());

(2)drawChild()

该方法用于具体调用子视图的draw方法,内部首先会处理视图动画(也就是说视图动画是在这里处理的),之后调用子视图的draw()。

从上面分析可以看出自定义viewGroup的时候需要最少覆写onMeasure()和onLayout()方法,其中onMeasure方法中可以直接调用measureChildren等已有的方法,而onLayout方法就需要设计者进行完整的定义;

一般不需要覆写dispatchDraw()和drawChild()这两个方法,因为上面两个方法已经完成了基本的事情。

但是可以通过覆写在该基础之上做一些特殊的效果,比如......(疯狂加载中......)

canvas.clipRect()方法

ClipView在重写dispatchDraw方法中用到了canvas.clipRect()方法,该方法用于裁剪画布,也就是设置画布的显示区域

调用clipRect()方法后,只会显示被裁剪的区域,之外的区域将不会显示

该方法最后有一个参数Region.Op,表示与之前区域的区域间运算种类,如果没有这个参数,则默认为Region.Op.INTERSECT

这几个参数的意义为:

DIFFERENCE //是第一次不同于第二次的部分显示出来 

REPLACE //是显示第二次的 

REVERSE_DIFFERENCE  //是第二次不同于第一次的部分显示 

INTERSECT //交集显示 

UNION //全部显示 

XOR //补集 就是全集的减去交集生育部分显示

你可能感兴趣的:(ViewPager3D库使用)