作者:一包纯牛奶

链接:

https://juejin.im/post/5d6bce24f265da03db0790d1

本文由作者授权发布。

这里我把作者两篇文章合体了,主要是为了在项目功能介绍的基础上,也可以直接看到原理实现,故文章略长,下面为文章正文。

最近公司项目在升级AndroidX,由于项目中用到的一些比较老的库都已停止更新维护,因此需要将这些库替换掉,其中就包括自动轮播的横幅库。

恰逢笔者在之前写过一个轮播图,因此就在此基础上重构,打造出了一个全新的支持多种样式的轮播库--- BannerViewPager。

https://github.com/zhpanvip/BannerViewPager/

BannerViewPager拥有简洁高效的代码,更是因为它高度的可定制性。BannerViewPager完全支持任意的页面布局,而且可以支持任意的Indicator样式。

甚至连Indicator的位置都可以做到任意摆放。是的,就是这么随心所欲。无图言叼,还是先通过图片和代码一览BannerViewPager的功能吧(多图预警)。

1个

BannerViewPager效果预览及API介绍

由于GIF图片质量问题,下面的预览图并不清晰,大家可以单击下面的链接或扫描二维码下载Apk体验。Apk放在github上,下载速度可能会比较慢。

https://github.com/zhpanvip/BannerViewPager/raw/master/download/app.apk

1. setIndicatorStyle(开局就放王炸?)

BannerViewPager目前内置了CIRCLE和DASH两个样式的指示器,通过setIndicatorStyle(int)一行代码就可以切换指示的样式。当然,如果内置样式不满足你的需求。

BannerViewPager还提供了自定义指针的功能。只要继承了BaseIndicatorView或实现IIndicator接口,并转换相应方法,就可以通过自定义视图为所欲为的打造任意的Indicator了。

如下图【自定义】就是自己实现的指示器样式。

圈:

短跑

自定义

下面通过代码演示如何切换指针:

mViewPager.setIndicatorStyle(IndicatorStyle.DASH).setIndicatorHeight(BannerUtils.dp2px(3f)).setIndicatorWidth(BannerUtils.dp2px(3), BannerUtils.dp2px(10)).setHolderCreator(() -> new ImageResourceViewHolder(0)).create(mDrawableList)

通过5行代码就轻松的实现了上图【Dash】仿造支付宝的Indicator样式(大家可以留意一下支付宝的轮播Indicator,挺有意思)。

关于自定义指标查看将放在后边章节详细讲解。

2.setPageStyle

通过setPageStyle(int)一行代码打开一屏三页模式,一屏三页模式下当前有某种样式,分别如下图所示:

多页

多页比例

多页重叠

代码演示:

mViewPager.setPageStyle(PageStyle.MULTI_PAGE).setPageMargin(BannerUtils.dp2px(10)).setRevealWidth(BannerUtils.dp2px(10)).setHolderCreator(() -> new ImageResourceViewHolder(BannerUtils.dp2px(5))).create(mDrawableList);

同样通过短短5行代码就实现了上图【MULTI_PAGE】的效果,简单好用!

3.如何实现指标位置任意摆放?

我们看到上面图表中MULTI_PAGE_OVERLAP模式下指示器显示到了横幅的下边。这种效果该怎么实现呢?

其实BannerViewPager是支持把指示器放到任意位置的。

之所以能如此强大是因为我们通过自定义指针替换了内置的IndicatorView,而此时的IndicatorView已经脱离了BannerViewPager,也就理所当然的可以放在任意位置了。

接下来通过代码来看下如何实现:

(1)Xml布局文件如下

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:app="http://schemas.android.com/apk/res-auto"android:layout_width="match_parent"android:layout_height="match_parent"><com.zhpan.bannerview.BannerViewPagerandroid:id="@+id/banner_view"android:layout_width="match_parent"android:layout_height="180dp"android:layout_marginTop="20dp"app:bvp_page_style="multi_page" /><com.zhpan.bannerview.indicator.CircleIndicatorViewandroid:id="@+id/indicator_view"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_below="@id/banner_view"android:layout_centerHorizontal="true"android:layout_marginTop="10dp" /></RelativeLayout>

(2)通过setIndicatorView(IIndicator)替换内部指针

CircleIndicatorView indicatorView = findViewById(R.id.indicator_view);
mViewPager.setIndicatorView(indicatorView).setIndicatorColor(Color.parseColor("#888888"),Color.parseColor("#118EEA")).setHolderCreator(() -> new ImageResourceViewHolder(BannerUtils.dp2px(5))).create(mDrawableList);

CircleIndicatorView是什么?

其实他就是内置在BannerViewPager中的指针,现在你只需要把它同BannerViewPager放在同一个布局文件中就可以了。又是仅通过一行代码就完成了对内部指针的替换,不知道你看完之后是否会拍案叫绝,竟然如此简单!

注:2.5.0版本中CircleIndicatorView与DashIndicatorView已被弃用,可以用IndicatorView来替代这两个指示器。IndicatorView承载了CIRCLE与DASH两种样式

4.setIndicatorSlideMode

我们应该看到过很多App轮播图的指示器都会跟随页面一起滑动。BannerViewPager自然也不会少了这个功能。通过setIndicatorSlideMode(int)一行代码就可以轻松切换到下图(SMOOTH)的效果。

当然,由于目前SMOOTH模式还存在一些BUG,所以现在还不推荐大家使用这一模式。不过在后续版本中我会尽力修复相关问题。

正常

光滑

代码实现仍然非常简单,使用BannerViewPager你只需要记住一个核心-只有一行!所以演示代码不再贴出你应该不会揍我吧?

5.setPageTransformerStyle

关于Transform更好的方式应该是留给开发者自己去实现,因此BannerViewPager中目前唯一内置了其中常用的Transform样式,如果不能满足需求,可以通过BannerViewPager的setPageTransformer(ViewPager.PageTransformer转换器)设置自定义的Transform 。

大致内置的转换样式如下:

手风琴

深度

罗特

当然,BannerViewPager的功能并不完全重叠,此后,更多功能就不再演示,可以看下面所有开放的API接口。

2

如何使用BannerViewPager

1. gradle中添加依赖

如果您已迁移到AndroidX请使用latestVersion(> = 2.4.3.1)

implementation 'com.zhpan.library:bannerview:latestVersion'

如果未迁移到AndroidX请使用(非Androidx的包托管在JCenter上):

implementation 'com.zhpan.library:bannerview:2.4.3.1'

2.在xml文件中添加如下代码:

<com.zhpan.bannerview.BannerViewPagerandroid:id="@+id/banner_view"android:layout_width="match_parent"android:layout_margin="10dp"android:layout_height="160dp" />

3.横幅的项目页面布局

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"android:layout_width="match_parent"android:layout_height="wrap_content"><ImageViewandroid:id="@+id/banner_image"android:layout_width="match_parent"android:layout_height="match_parent"android:scaleType="centerCrop" /><LinearLayoutandroid:layout_width="match_parent"android:layout_height="wrap_content"android:layout_alignParentBottom="true"android:background="#66000000"android:gravity="center_vertical"><TextViewandroid:id="@+id/tv_describe"android:layout_width="wrap_content"android:layout_height="match_parent"android:layout_gravity="center_vertical"android:layout_marginStart="15dp"android:gravity="center_vertical"android:paddingTop="5dp"android:paddingBottom="5dp"android:textColor="#FFFFFF"android:textSize="16sp" /></LinearLayout></RelativeLayout>

4.自定义ViewHolder

public class NetViewHolder implements ViewHolder<BannerData> {private ImageView mImageView;private TextView mTextView;@Overridepublic View createView(ViewGroup viewGroup, Context context, int position) {View view = LayoutInflater.from(context).inflate(R.layout.item_net, viewGroup, false);mImageView = view.findViewById(R.id.banner_image);mTextView = view.findViewById(R.id.tv_describe);return view;}@Overridepublic void onBind(Context context, BannerData data, int position, int size) {ImageLoaderOptions options = new ImageLoaderOptions.Builder().into(mImageView).load(data.getImagePath()).placeHolder(R.drawable.placeholder).build();ImageLoaderManager.getInstance().loadImage(options);mTextView.setText(data.getTitle());}
}

5.BannerViewPager参数配置

private BannerViewPager<BannerData, NetViewHolder> mBannerViewPager;
private void initViewPager() {mBannerViewPager = findViewById(R.id.banner_view);mBannerViewPager.showIndicator(true).setInterval(3000).setCanLoop(false).setAutoPlay(true).setRoundCorner(DpUtils.dp2px(7)).setIndicatorColor(Color.parseColor("#935656"), Color.parseColor("#FF4C39")).setIndicatorGravity(BannerViewPager.END).setScrollDuration(1000).setHolderCreator(NetViewHolder::new).setOnPageClickListener(position -> {BannerData bannerData = mBannerViewPager.getList().get(position);Toast.makeText(NetworkBannerActivity.this,"点击了图片" + position + " " + bannerData.getDesc(), Toast.LENGTH_SHORT).show();}).create(mList);
}

6.开启与停止轮播

2.5.0之后版本无需自行在活动或片段中管理stopLoop和startLoop方法,则两个方法依旧保留对外开发

如果开启了自动轮播功能,请限制在onDestroy中停止轮播,以免出现内存泄漏。

@Override
protected void onDestroy() {super.onDestroy();if (mBannerViewPager != null)mViewpager.stopLoop();
}

为了节省性能也可以在onStop中停止轮播,在onResume中开启轮播:

@Override
protected void onStop() {super.onStop();if (mBannerViewPager != null)mBannerViewPager.stopLoop();
}@Override
protected void onResume() {super.onResume();if (mBannerViewPager != null)mBannerViewPager.startLoop();
}

3

如何支持任意的项目布局

产品的需求千变万化,你永远也猜不到下一步产品会给你提一个什么样的需求。

因此对于一个比较人性化的横幅库来说,它也应该支持开发者去自定义任意的项目页面布局。BannerViewPager就是本着这样的思路来做的。

那么究竟其内部是如何实现的呢?

我们先从setHolderCreator(HolderCreatorholderCreator)这个方法说起。

在使用BannerViewPager的时候我们可以为此设置一个HolderCreator,代码如下:

bannerViewPager.setHolderCreator(new HolderCreator<CustomPageViewHolder>() {@Overridepublic CustomPageViewHolder createViewHolder() {return new CustomPageViewHolder();}
})

而在HolderCreator的createViewHolder方法中返回了一个CustomPageViewHolder,这个CustomPageViewHolder是我们自己实现的。其内部通过createView方法来inflate出来一个我们自定义的itemView,并在绑定方法中为itemView绑定数据。

其代码如下:

public class CustomPageViewHolder implements ViewHolder<CustomBean> {private ImageView mImageView;private TextView mTextView;@Overridepublic View createView(ViewGroup viewGroup, Context context, int position) {View view = LayoutInflater.from(context).inflate(R.layout.item_custom_view, viewGroup, false);mImageView = view.findViewById(R.id.banner_image);mTextView = view.findViewById(R.id.tv_describe);return view;}@Overridepublic void onBind(Context context, CustomBean data, int position, int size) {mImageView.setImageResource(data.getImageRes());mTextView.setText(data.getImageDescription());}...
}

setHolderCreator之后再BannerViewPager内部是如何处理的呢?我们接下来继续看:

/*** 必须为BannerViewPager设置HolderCreator,HolderCreator中创建ViewHolder,* 在ViewHolder中管理BannerViewPager的ItemView.** @param holderCreator HolderCreator*/
public BannerViewPager<T, VH> setHolderCreator(HolderCreator<VH> holderCreator) {this.holderCreator = holderCreator;return this;
}private void setupViewPager() {if (holderCreator != null) {BannerPagerAdapter<T, VH> bannerPagerAdapter =new BannerPagerAdapter<>(mList, holderCreator);...} else {throw new NullPointerException("You must set HolderCreator for BannerViewPager");}}

上述代码中判断如果holderCreator为null时就抛出了一个NullPointerException,这也解释了为什么必须要为BannenrViewPager设置holderCreator。

当holder创建者不为空时,将holder传递到了BannerPagerAdapter中。我们接下来到BannerPagerAdapter中一探究竟:

public class BannerPagerAdapter<T, VH extends ViewHolder> extends PagerAdapter {@Overridepublic @NonNullObject instantiateItem(@NonNull final ViewGroup container, final int position) {//  为了方便理解,此处与源码并不一致,详情请参看BannerPagerAdapter源码View itemView =getView(position, container);container.addView(itemView);return itemView;}...@SuppressWarnings("unchecked")private View getView(final int position, ViewGroup container) {ViewHolder<T> holder = holderCreator.createViewHolder();if (holder == null) {throw new RuntimeException("can not return a null holder");}return createView(holder, position, container);}private View createView(ViewHolder<T> holder, int position, ViewGroup container) {View view = null;if (list != null && list.size() > 0) {view = holder.createView(container, container.getContext(), position);holder.onBind(container.getContext(), list.get(position), position, list.size());return view;}}

在BannerPagerAdapter的getView方法中通过holderCreator.createViewHolder()拿到了我们自定义的ViewHolder,此时即为上边的CustomPageViewHolder。

接下来在createView方法中调用CustomPageViewHolder的createView方法拿到我们自定义的itemView,并通过holder.onBind方法将集合中的数据传递给了CustomPageViewHolder。

到这里我们就完成了自定义item布局以及item数据的绑定。

4

BannerViewPager的泛型设计

上一级中讲解了如何通过HolderCreator来支持任意的页面布局,那么此时我们应该会面对一个难点,既然可以支持任意的页面布局那么BannerViewPager中接收的数据也应该时时任意类型的。我们可以约会泛型来实现。

首先看BannerViewPager的泛型:

public class BannerViewPager<T, VH extends ViewHolder> extends RelativeLayout implementsViewPager.OnPageChangeListener {// 轮播数据集合private List<T> mList;private HolderCreator<VH> holderCreator;//  ...
}

BannerViewPager有两个泛型参数,第一个参数T是对应的数据类型,它用作BannerViewPager中List集合的泛型。

另一个通用型参数VH规定了必须是继承的ViewHolder的类,用作作为HolderCreator的通用型。而ViewHolder和HolderCreator均是一个带有通用型参数的接口,其代码如下:

public interface ViewHolder<T> {View createView(ViewGroup viewGroup,Context context, int position);/*** @param context context* @param data 实体类对象* @param position 当前位置* @param size 页面个数*/void onBind(Context context,T data,int position,int size);
}public interface HolderCreator<VH extends ViewHolder> {/*** 创建ViewHolder*/VH createViewHolder();
}

另外,T和VH两个泛型也同时作为BannerPagerAdapter的泛型参数:

public class BannerPagerAdapter<T, VH extends ViewHolder> extends PagerAdapter {private List<T> list;public BannerPagerAdapter(List<T> list, HolderCreator<VH> holderCreator) {this.list = list;this.holderCreator = holderCreator;}}

可以看到,我们通过泛型约束,做到涉及到的相关类中的参数数据类型保持了同步,从而实现了BannerViewPager可以接收并处理任意的数据类型。

5

如何实现无限循环轮播

关于ViewPager的无限循环无外乎两种方案。

第一种方案是在PagerAdapter的getCount中返回一个Integer.MAX_VALUE,即一个最大的Integer整数。然后将setCurrentItem的值设置为Integer.MAX_VALUE / 2,在滑动过程中不断取余剩余由此来达到一个无限循环轮播的假象。

另外一种方案是额外增加两个ViewPager的项目计数,然后在第0个项目填充最后一条数据,在最后一个项目填充第0条数据。当右滑到第一个项目的时候将currentItem放置为pageSize -1,当滑动到最后一个项目的时候将currentItem放置为1,从而达到一个无限循环的目的,此方案的示意图如下:

BannerViewPager也从方案二转向了方案一。

所以简单聊几句方案二缺点。

方案二的缺点:

这个方案的优点虽然我苦思冥想也只想出来了两条,但是关于它的缺点我却能罗列出来很多。正所谓谁(sei)用谁(sei)知道!

1.onPageSelected(int)方法重复调用问题

我们为BannerViewPager打开自动轮播,并为此设置页面更改的监听事件,如下:

mBannerViewPager.setAutoPlay(true)
.setOnPageChangeListener(new OnPageChangeListenerAdapter() {@Overridepublic void onPageSelected(int position) {BannerUtils.e("position " + position);}
})

然后可以看到打印的Log:

在BannerViewPager只有三个页面的情况下,页面位置选择的周期是0、1、2、0。很明显,第0个页面被多调用了一次。

虽然在大多数情况下并没有影响,但是当需要在某些第0个页面时做一些逻辑的话,就会产生一定的影响。

2.一屏三页模式下,这一方案在轮播到最后一页时会出现下一页短暂的空白的问题

出现这一问题的原因是因为为了完成循环在切换到最后一页时我们立即将位置切换到了位置为1的页面,而此时位置为2个页面不再加载出来,因此就有了短暂的空白问题。

为了解决这一问题,又不得不在原来循环的基础上再增加两个页面,设置setOffscreenPageLimit设置为2。这样无形中增加了内存消耗,并且使逻辑处理变得更加复杂!

3.需要对位置进行变换

为了实现循环我们将页面计数增加了2,以便解决一屏三页的空白问题我们将页面计数增加了4。但对外暴露的接口需要拿到正确的位置,此时我们就不得不在BannerViewPager内部对位置进行变换,使之能够对应到正确的位置。

6

遇到的其他问题及解决方案

在BannerViewPager的开发过避免不了的会碰到一些问题,虽然有些已经解决了,但有些可能还悬而未决。但是不管解决没解决以供大家参考或讨论。

1.手指滑动页面过程中应停止自动轮播

自动轮播的功能是通过Handler来实现的。通过postDelayed开启轮播,通过removeCallbacks停止轮播。

代码如下:


/*** 开启轮播*/
public void startLoop() {if (!isLooping && isAutoPlay && mList.size() > 1) {mHandler.postDelayed(mRunnable, interval);isLooping = true;}
}/*** 停止轮播*/
public void stopLoop() {if (isLooping) {mHandler.removeCallbacks(mRunnable);isLooping = false;}
}

如果在手指滑动的过程中没有停止轮播,体验上来说非常不好。因此,需要处理这种情况。解决方案是重置ViewPager的setOnTouchListener方法,监听手指滑动的时候停止轮播,抬起手指的时候开启轮播。

代码如下:

 private void setTouchListener() {mViewPager.setOnTouchListener((v, event) -> {int action = event.getAction();switch (action) {case MotionEvent.ACTION_DOWN:case MotionEvent.ACTION_MOVE:isLooping = true;stopLoop();break;case MotionEvent.ACTION_UP:case MotionEvent.ACTION_CANCEL:isLooping = false;startLoop();default:break;}return false;});}

2.关于instantiateItem的优化问题

我们知道,在ViewPager每次切换页面的时候都会调用instantiateItem去实例化ItemView,也就意味着我们在这个方法中通过ViewHolder的createView方法每次切换页面都会被重新初始化绑定数据。这样对程序来说是一种性能上的浪费。

针对这种情况,在2.4.3之前的版本中做了些优化。在BannerPagerAdapter中维护一个列表mViewList集合,用于存放放置出来的itemView。在itemView初始化成功后,由此设置标签并保存到集合中,当在此之后切换页面时我们从集合中取出itemView并进行比较,如果一致则直接使用即可。

这样就避免了重复的创建对象,造成一些性能提升。具体代码如下:


public class BannerPagerAdapter<T, VH extends ViewHolder> extends PagerAdapter {private List<View> mViewList = new ArrayList<>();private View findViewByPosition(ViewGroup container, int position) {for (View view : mViewList) {if (((int) view.getTag()) == position && view.getParent() == null) {return view;}}View view = getView(position, container);view.setTag(position);mViewList.add(view);return view;}
}

但是这一优化却又会引发另一个问题,即内存问题!

通常应用程序的轮播控件都是图片,而图片是比较占用内存的。此时我们把所有的itemView都存储在一个集合中这样真的是一个好的方案吗?

在ViewPager页面少的情况下问题可能不会凸显。但是如果ViewPager的页面很多的情况下问题就相当严重了!

于是,后来我灵光一闪,突然奇想!那我就设置一个最大缓存呗?当集合中的个数超过阈值的时候就把最近用过的一个项目视图移除掉不就好了?

妙哉妙哉!可转念一想,这尼玛和设置一个setOffscreenPageLimit有什么区别呢?

当我们在考虑这些问题的时候Google工程师早就替我们想到了!所以关于ViewPager的instantiateItem是否有必要去优化我目前持有的态度。

但是,在BannerViewPager 2.4.3的版本中确实确实做了上述优化,因此前一些版本中可能会存在内存问题。至于2.4.3或之后版本大概会去掉这部分优化。这个问题可能也只能留在未来,待升级到ViewPager2后解决了!关于这个问题欢迎大家在文章下方留言,各抒己见!

3.RecyclerView + ViewPager会有非平滑的页面滑动情况

这个问题不是太好描述,我们直接通过一张GIF来看

从图中可以很直观的看到,把BannerViewPager向上划出屏幕再很快划回来,此时BannerViewPager页面切换的动画没有了,很生硬的直接跳到了下一页。

这个问题不是BannerViewPager的错误,而是ViewPager内部原因导致的,可以看到很多线上的APP都存在这个问题,例如喜马拉雅(喜马拉雅的轮播图真心做的好看呀,效果也很赞!)。

这个bug虽然不影响使用,但是总感觉效果不太好。因此还是要处理一下。处理之前先分析一下问题原因。

在ViewPager内部有一个私有成员变量mFirstLayout,其默认值是true。这个参数标记为第一次布局的。如果是第一次布局那么滑动就不是平滑的。

代码如下:

public void setCurrentItem(int item) {mPopulatePending = false;setCurrentItemInternal(item, !mFirstLayout, false);
}void setCurrentItemInternal(int item, boolean smoothScroll, boolean always) {setCurrentItemInternal(item, smoothScroll, always, 0);
}

这个参数在onLayout方法中会被置为false。代码如下:

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {//  在onLayout的最后一行mFirstLayout = false;
}

因此,在正常情况下,onLayout执行之后页面滑动都应该是smooth的。然后,当ViewPager滑动出屏幕的时候其在DetachedFromWindow方法会被调用,而当其再次进入屏幕的时候则调用到AttachedToWindow这个方法。

来看看onAttachedToWindow方法中的代码:

@Override
protected void onAttachedToWindow() {super.onAttachedToWindow();mFirstLayout = true;
}

只是把mFirstLayout放置为true!而如果此时onLayout没有被触发,则先发生了页面滚动,那么此时的页面滑动就没了的平滑效果了。

了解了原因之后处理起来就简单了,因为mFirstLayout是专有属性,我们无法访问,所以只能通过反射来修改其值。我们在CatchViewPager(继承自ViewPager的一个类)中做如下操作:


private boolean firstLayout = true;@Overrideprotected void onAttachedToWindow() {super.onAttachedToWindow();hookFirstLayout();}@Overrideprotected void onDetachedFromWindow() {super.onDetachedFromWindow();firstLayout = false;}private void hookFirstLayout() {try {Field mFirstLayout = ViewPager.class.getDeclaredField("mFirstLayout");mFirstLayout.setAccessible(true);mFirstLayout.set(this, firstLayout);setCurrentItem(getCurrentItem());} catch (IllegalAccessException | NoSuchFieldException e) {e.printStackTrace();}}

这样问题就迎刃而解了,再次滑动RecyclerView,一切完好!

作者:一包纯牛奶

链接:

https://juejin.im/post/5d6bce24f265da03db0790d1

本文由作者授权发布。

这里我把作者两篇文章合体了,主要是为了在项目功能介绍的基础上,也可以直接看到原理实现,故文章略长,下面为文章正文。

最近公司项目在升级AndroidX,由于项目中用到的一些比较老的库都已停止更新维护,因此需要将这些库替换掉,其中就包括自动轮播的横幅库。

恰逢笔者在之前写过一个轮播图,因此就在此基础上重构,打造出了一个全新的支持多种样式的轮播库--- BannerViewPager。

https://github.com/zhpanvip/BannerViewPager/

BannerViewPager拥有简洁高效的代码,更是因为它高度的可定制性。BannerViewPager完全支持任意的页面布局,而且可以支持任意的Indicator样式。

甚至连Indicator的位置都可以做到任意摆放。是的,就是这么随心所欲。无图言叼,还是先通过图片和代码一览BannerViewPager的功能吧(多图预警)。

1个

BannerViewPager效果预览及API介绍

由于GIF图片质量问题,下面的预览图并不清晰,大家可以单击下面的链接或扫描二维码下载Apk体验。Apk放在github上,下载速度可能会比较慢。

https://github.com/zhpanvip/BannerViewPager/raw/master/download/app.apk

1. setIndicatorStyle(开局就放王炸?)

BannerViewPager目前内置了CIRCLE和DASH两个样式的指示器,通过setIndicatorStyle(int)一行代码就可以切换指示的样式。当然,如果内置样式不满足你的需求。

BannerViewPager还提供了自定义指针的功能。只要继承了BaseIndicatorView或实现IIndicator接口,并转换相应方法,就可以通过自定义视图为所欲为的打造任意的Indicator了。

如下图【自定义】就是自己实现的指示器样式。

圈:

短跑

自定义

下面通过代码演示如何切换指针:

mViewPager.setIndicatorStyle(IndicatorStyle.DASH).setIndicatorHeight(BannerUtils.dp2px(3f)).setIndicatorWidth(BannerUtils.dp2px(3), BannerUtils.dp2px(10)).setHolderCreator(() -> new ImageResourceViewHolder(0)).create(mDrawableList)

通过5行代码就轻松的实现了上图【Dash】仿造支付宝的Indicator样式(大家可以留意一下支付宝的轮播Indicator,挺有意思)。

关于自定义指标查看将放在后边章节详细讲解。

2.setPageStyle

通过setPageStyle(int)一行代码打开一屏三页模式,一屏三页模式下当前有某种样式,分别如下图所示:

多页

多页比例

多页重叠

代码演示:

mViewPager.setPageStyle(PageStyle.MULTI_PAGE).setPageMargin(BannerUtils.dp2px(10)).setRevealWidth(BannerUtils.dp2px(10)).setHolderCreator(() -> new ImageResourceViewHolder(BannerUtils.dp2px(5))).create(mDrawableList);

同样通过短短5行代码就实现了上图【MULTI_PAGE】的效果,简单好用!

3.如何实现指标位置任意摆放?

我们看到上面图表中MULTI_PAGE_OVERLAP模式下指示器显示到了横幅的下边。这种效果该怎么实现呢?

其实BannerViewPager是支持把指示器放到任意位置的。

之所以能如此强大是因为我们通过自定义指针替换了内置的IndicatorView,而此时的IndicatorView已经脱离了BannerViewPager,也就理所当然的可以放在任意位置了。

接下来通过代码来看下如何实现:

(1)Xml布局文件如下

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:app="http://schemas.android.com/apk/res-auto"android:layout_width="match_parent"android:layout_height="match_parent"><com.zhpan.bannerview.BannerViewPagerandroid:id="@+id/banner_view"android:layout_width="match_parent"android:layout_height="180dp"android:layout_marginTop="20dp"app:bvp_page_style="multi_page" /><com.zhpan.bannerview.indicator.CircleIndicatorViewandroid:id="@+id/indicator_view"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_below="@id/banner_view"android:layout_centerHorizontal="true"android:layout_marginTop="10dp" /></RelativeLayout>

(2)通过setIndicatorView(IIndicator)替换内部指针

CircleIndicatorView indicatorView = findViewById(R.id.indicator_view);
mViewPager.setIndicatorView(indicatorView).setIndicatorColor(Color.parseColor("#888888"),Color.parseColor("#118EEA")).setHolderCreator(() -> new ImageResourceViewHolder(BannerUtils.dp2px(5))).create(mDrawableList);

CircleIndicatorView是什么?

其实他就是内置在BannerViewPager中的指针,现在你只需要把它同BannerViewPager放在同一个布局文件中就可以了。又是仅通过一行代码就完成了对内部指针的替换,不知道你看完之后是否会拍案叫绝,竟然如此简单!

注:2.5.0版本中CircleIndicatorView与DashIndicatorView已被弃用,可以用IndicatorView来替代这两个指示器。IndicatorView承载了CIRCLE与DASH两种样式

4.setIndicatorSlideMode

我们应该看到过很多App轮播图的指示器都会跟随页面一起滑动。BannerViewPager自然也不会少了这个功能。通过setIndicatorSlideMode(int)一行代码就可以轻松切换到下图(SMOOTH)的效果。

当然,由于目前SMOOTH模式还存在一些BUG,所以现在还不推荐大家使用这一模式。不过在后续版本中我会尽力修复相关问题。

正常

光滑

代码实现仍然非常简单,使用BannerViewPager你只需要记住一个核心-只有一行!所以演示代码不再贴出你应该不会揍我吧?

5.setPageTransformerStyle

关于Transform更好的方式应该是留给开发者自己去实现,因此BannerViewPager中目前唯一内置了其中常用的Transform样式,如果不能满足需求,可以通过BannerViewPager的setPageTransformer(ViewPager.PageTransformer转换器)设置自定义的Transform 。

大致内置的转换样式如下:

手风琴

深度

罗特

当然,BannerViewPager的功能并不完全重叠,此后,更多功能就不再演示,可以看下面所有开放的API接口。

2

如何使用BannerViewPager

1. gradle中添加依赖

如果您已迁移到AndroidX请使用latestVersion(> = 2.4.3.1)

implementation 'com.zhpan.library:bannerview:latestVersion'

如果未迁移到AndroidX请使用(非Androidx的包托管在JCenter上):

implementation 'com.zhpan.library:bannerview:2.4.3.1'

2.在xml文件中添加如下代码:

<com.zhpan.bannerview.BannerViewPagerandroid:id="@+id/banner_view"android:layout_width="match_parent"android:layout_margin="10dp"android:layout_height="160dp" />

3.横幅的项目页面布局

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"android:layout_width="match_parent"android:layout_height="wrap_content"><ImageViewandroid:id="@+id/banner_image"android:layout_width="match_parent"android:layout_height="match_parent"android:scaleType="centerCrop" /><LinearLayoutandroid:layout_width="match_parent"android:layout_height="wrap_content"android:layout_alignParentBottom="true"android:background="#66000000"android:gravity="center_vertical"><TextViewandroid:id="@+id/tv_describe"android:layout_width="wrap_content"android:layout_height="match_parent"android:layout_gravity="center_vertical"android:layout_marginStart="15dp"android:gravity="center_vertical"android:paddingTop="5dp"android:paddingBottom="5dp"android:textColor="#FFFFFF"android:textSize="16sp" /></LinearLayout></RelativeLayout>

4.自定义ViewHolder

public class NetViewHolder implements ViewHolder<BannerData> {private ImageView mImageView;private TextView mTextView;@Overridepublic View createView(ViewGroup viewGroup, Context context, int position) {View view = LayoutInflater.from(context).inflate(R.layout.item_net, viewGroup, false);mImageView = view.findViewById(R.id.banner_image);mTextView = view.findViewById(R.id.tv_describe);return view;}@Overridepublic void onBind(Context context, BannerData data, int position, int size) {ImageLoaderOptions options = new ImageLoaderOptions.Builder().into(mImageView).load(data.getImagePath()).placeHolder(R.drawable.placeholder).build();ImageLoaderManager.getInstance().loadImage(options);mTextView.setText(data.getTitle());}
}

5.BannerViewPager参数配置

private BannerViewPager<BannerData, NetViewHolder> mBannerViewPager;
private void initViewPager() {mBannerViewPager = findViewById(R.id.banner_view);mBannerViewPager.showIndicator(true).setInterval(3000).setCanLoop(false).setAutoPlay(true).setRoundCorner(DpUtils.dp2px(7)).setIndicatorColor(Color.parseColor("#935656"), Color.parseColor("#FF4C39")).setIndicatorGravity(BannerViewPager.END).setScrollDuration(1000).setHolderCreator(NetViewHolder::new).setOnPageClickListener(position -> {BannerData bannerData = mBannerViewPager.getList().get(position);Toast.makeText(NetworkBannerActivity.this,"点击了图片" + position + " " + bannerData.getDesc(), Toast.LENGTH_SHORT).show();}).create(mList);
}

6.开启与停止轮播

2.5.0之后版本无需自行在活动或片段中管理stopLoop和startLoop方法,则两个方法依旧保留对外开发

如果开启了自动轮播功能,请限制在onDestroy中停止轮播,以免出现内存泄漏。

@Override
protected void onDestroy() {super.onDestroy();if (mBannerViewPager != null)mViewpager.stopLoop();
}

为了节省性能也可以在onStop中停止轮播,在onResume中开启轮播:

@Override
protected void onStop() {super.onStop();if (mBannerViewPager != null)mBannerViewPager.stopLoop();
}@Override
protected void onResume() {super.onResume();if (mBannerViewPager != null)mBannerViewPager.startLoop();
}

3

如何支持任意的项目布局

产品的需求千变万化,你永远也猜不到下一步产品会给你提一个什么样的需求。

因此对于一个比较人性化的横幅库来说,它也应该支持开发者去自定义任意的项目页面布局。BannerViewPager就是本着这样的思路来做的。

那么究竟其内部是如何实现的呢?

我们先从setHolderCreator(HolderCreatorholderCreator)这个方法说起。

在使用BannerViewPager的时候我们可以为此设置一个HolderCreator,代码如下:

bannerViewPager.setHolderCreator(new HolderCreator<CustomPageViewHolder>() {@Overridepublic CustomPageViewHolder createViewHolder() {return new CustomPageViewHolder();}
})

而在HolderCreator的createViewHolder方法中返回了一个CustomPageViewHolder,这个CustomPageViewHolder是我们自己实现的。其内部通过createView方法来inflate出来一个我们自定义的itemView,并在绑定方法中为itemView绑定数据。

其代码如下:

public class CustomPageViewHolder implements ViewHolder<CustomBean> {private ImageView mImageView;private TextView mTextView;@Overridepublic View createView(ViewGroup viewGroup, Context context, int position) {View view = LayoutInflater.from(context).inflate(R.layout.item_custom_view, viewGroup, false);mImageView = view.findViewById(R.id.banner_image);mTextView = view.findViewById(R.id.tv_describe);return view;}@Overridepublic void onBind(Context context, CustomBean data, int position, int size) {mImageView.setImageResource(data.getImageRes());mTextView.setText(data.getImageDescription());}...
}

setHolderCreator之后再BannerViewPager内部是如何处理的呢?我们接下来继续看:

/*** 必须为BannerViewPager设置HolderCreator,HolderCreator中创建ViewHolder,* 在ViewHolder中管理BannerViewPager的ItemView.** @param holderCreator HolderCreator*/
public BannerViewPager<T, VH> setHolderCreator(HolderCreator<VH> holderCreator) {this.holderCreator = holderCreator;return this;
}private void setupViewPager() {if (holderCreator != null) {BannerPagerAdapter<T, VH> bannerPagerAdapter =new BannerPagerAdapter<>(mList, holderCreator);...} else {throw new NullPointerException("You must set HolderCreator for BannerViewPager");}}

上述代码中判断如果holderCreator为null时就抛出了一个NullPointerException,这也解释了为什么必须要为BannenrViewPager设置holderCreator。

当holder创建者不为空时,将holder传递到了BannerPagerAdapter中。我们接下来到BannerPagerAdapter中一探究竟:

public class BannerPagerAdapter<T, VH extends ViewHolder> extends PagerAdapter {@Overridepublic @NonNullObject instantiateItem(@NonNull final ViewGroup container, final int position) {//  为了方便理解,此处与源码并不一致,详情请参看BannerPagerAdapter源码View itemView =getView(position, container);container.addView(itemView);return itemView;}...@SuppressWarnings("unchecked")private View getView(final int position, ViewGroup container) {ViewHolder<T> holder = holderCreator.createViewHolder();if (holder == null) {throw new RuntimeException("can not return a null holder");}return createView(holder, position, container);}private View createView(ViewHolder<T> holder, int position, ViewGroup container) {View view = null;if (list != null && list.size() > 0) {view = holder.createView(container, container.getContext(), position);holder.onBind(container.getContext(), list.get(position), position, list.size());return view;}}

在BannerPagerAdapter的getView方法中通过holderCreator.createViewHolder()拿到了我们自定义的ViewHolder,此时即为上边的CustomPageViewHolder。

接下来在createView方法中调用CustomPageViewHolder的createView方法拿到我们自定义的itemView,并通过holder.onBind方法将集合中的数据传递给了CustomPageViewHolder。

到这里我们就完成了自定义item布局以及item数据的绑定。

4

BannerViewPager的泛型设计

上一级中讲解了如何通过HolderCreator来支持任意的页面布局,那么此时我们应该会面对一个难点,既然可以支持任意的页面布局那么BannerViewPager中接收的数据也应该时时任意类型的。我们可以约会泛型来实现。

首先看BannerViewPager的泛型:

public class BannerViewPager<T, VH extends ViewHolder> extends RelativeLayout implementsViewPager.OnPageChangeListener {// 轮播数据集合private List<T> mList;private HolderCreator<VH> holderCreator;//  ...
}

BannerViewPager有两个泛型参数,第一个参数T是对应的数据类型,它用作BannerViewPager中List集合的泛型。

另一个通用型参数VH规定了必须是继承的ViewHolder的类,用作作为HolderCreator的通用型。而ViewHolder和HolderCreator均是一个带有通用型参数的接口,其代码如下:

public interface ViewHolder<T> {View createView(ViewGroup viewGroup,Context context, int position);/*** @param context context* @param data 实体类对象* @param position 当前位置* @param size 页面个数*/void onBind(Context context,T data,int position,int size);
}public interface HolderCreator<VH extends ViewHolder> {/*** 创建ViewHolder*/VH createViewHolder();
}

另外,T和VH两个泛型也同时作为BannerPagerAdapter的泛型参数:

public class BannerPagerAdapter<T, VH extends ViewHolder> extends PagerAdapter {private List<T> list;public BannerPagerAdapter(List<T> list, HolderCreator<VH> holderCreator) {this.list = list;this.holderCreator = holderCreator;}}

可以看到,我们通过泛型约束,做到涉及到的相关类中的参数数据类型保持了同步,从而实现了BannerViewPager可以接收并处理任意的数据类型。

5

如何实现无限循环轮播

关于ViewPager的无限循环无外乎两种方案。

第一种方案是在PagerAdapter的getCount中返回一个Integer.MAX_VALUE,即一个最大的Integer整数。然后将setCurrentItem的值设置为Integer.MAX_VALUE / 2,在滑动过程中不断取余剩余由此来达到一个无限循环轮播的假象。

另外一种方案是额外增加两个ViewPager的项目计数,然后在第0个项目填充最后一条数据,在最后一个项目填充第0条数据。当右滑到第一个项目的时候将currentItem放置为pageSize -1,当滑动到最后一个项目的时候将currentItem放置为1,从而达到一个无限循环的目的,此方案的示意图如下:

BannerViewPager也从方案二转向了方案一。

所以简单聊几句方案二缺点。

方案二的缺点:

这个方案的优点虽然我苦思冥想也只想出来了两条,但是关于它的缺点我却能罗列出来很多。正所谓谁(sei)用谁(sei)知道!

1.onPageSelected(int)方法重复调用问题

我们为BannerViewPager打开自动轮播,并为此设置页面更改的监听事件,如下:

mBannerViewPager.setAutoPlay(true)
.setOnPageChangeListener(new OnPageChangeListenerAdapter() {@Overridepublic void onPageSelected(int position) {BannerUtils.e("position " + position);}
})

然后可以看到打印的Log:

在BannerViewPager只有三个页面的情况下,页面位置选择的周期是0、1、2、0。很明显,第0个页面被多调用了一次。

虽然在大多数情况下并没有影响,但是当需要在某些第0个页面时做一些逻辑的话,就会产生一定的影响。

2.一屏三页模式下,这一方案在轮播到最后一页时会出现下一页短暂的空白的问题

出现这一问题的原因是因为为了完成循环在切换到最后一页时我们立即将位置切换到了位置为1的页面,而此时位置为2个页面不再加载出来,因此就有了短暂的空白问题。

为了解决这一问题,又不得不在原来循环的基础上再增加两个页面,设置setOffscreenPageLimit设置为2。这样无形中增加了内存消耗,并且使逻辑处理变得更加复杂!

3.需要对位置进行变换

为了实现循环我们将页面计数增加了2,以便解决一屏三页的空白问题我们将页面计数增加了4。但对外暴露的接口需要拿到正确的位置,此时我们就不得不在BannerViewPager内部对位置进行变换,使之能够对应到正确的位置。

6

遇到的其他问题及解决方案

在BannerViewPager的开发过避免不了的会碰到一些问题,虽然有些已经解决了,但有些可能还悬而未决。但是不管解决没解决以供大家参考或讨论。

1.手指滑动页面过程中应停止自动轮播

自动轮播的功能是通过Handler来实现的。通过postDelayed开启轮播,通过removeCallbacks停止轮播。

代码如下:


/*** 开启轮播*/
public void startLoop() {if (!isLooping && isAutoPlay && mList.size() > 1) {mHandler.postDelayed(mRunnable, interval);isLooping = true;}
}/*** 停止轮播*/
public void stopLoop() {if (isLooping) {mHandler.removeCallbacks(mRunnable);isLooping = false;}
}

如果在手指滑动的过程中没有停止轮播,体验上来说非常不好。因此,需要处理这种情况。解决方案是重置ViewPager的setOnTouchListener方法,监听手指滑动的时候停止轮播,抬起手指的时候开启轮播。

代码如下:

 private void setTouchListener() {mViewPager.setOnTouchListener((v, event) -> {int action = event.getAction();switch (action) {case MotionEvent.ACTION_DOWN:case MotionEvent.ACTION_MOVE:isLooping = true;stopLoop();break;case MotionEvent.ACTION_UP:case MotionEvent.ACTION_CANCEL:isLooping = false;startLoop();default:break;}return false;});}

2.关于instantiateItem的优化问题

我们知道,在ViewPager每次切换页面的时候都会调用instantiateItem去实例化ItemView,也就意味着我们在这个方法中通过ViewHolder的createView方法每次切换页面都会被重新初始化绑定数据。这样对程序来说是一种性能上的浪费。

针对这种情况,在2.4.3之前的版本中做了些优化。在BannerPagerAdapter中维护一个列表mViewList集合,用于存放放置出来的itemView。在itemView初始化成功后,由此设置标签并保存到集合中,当在此之后切换页面时我们从集合中取出itemView并进行比较,如果一致则直接使用即可。

这样就避免了重复的创建对象,造成一些性能提升。具体代码如下:


public class BannerPagerAdapter<T, VH extends ViewHolder> extends PagerAdapter {private List<View> mViewList = new ArrayList<>();private View findViewByPosition(ViewGroup container, int position) {for (View view : mViewList) {if (((int) view.getTag()) == position && view.getParent() == null) {return view;}}View view = getView(position, container);view.setTag(position);mViewList.add(view);return view;}
}

但是这一优化却又会引发另一个问题,即内存问题!

通常应用程序的轮播控件都是图片,而图片是比较占用内存的。此时我们把所有的itemView都存储在一个集合中这样真的是一个好的方案吗?

在ViewPager页面少的情况下问题可能不会凸显。但是如果ViewPager的页面很多的情况下问题就相当严重了!

于是,后来我灵光一闪,突然奇想!那我就设置一个最大缓存呗?当集合中的个数超过阈值的时候就把最近用过的一个项目视图移除掉不就好了?

妙哉妙哉!可转念一想,这尼玛和设置一个setOffscreenPageLimit有什么区别呢?

当我们在考虑这些问题的时候Google工程师早就替我们想到了!所以关于ViewPager的instantiateItem是否有必要去优化我目前持有的态度。

但是,在BannerViewPager 2.4.3的版本中确实确实做了上述优化,因此前一些版本中可能会存在内存问题。至于2.4.3或之后版本大概会去掉这部分优化。这个问题可能也只能留在未来,待升级到ViewPager2后解决了!关于这个问题欢迎大家在文章下方留言,各抒己见!

3.RecyclerView + ViewPager会有非平滑的页面滑动情况

这个问题不是太好描述,我们直接通过一张GIF来看

从图中可以很直观的看到,把BannerViewPager向上划出屏幕再很快划回来,此时BannerViewPager页面切换的动画没有了,很生硬的直接跳到了下一页。

这个问题不是BannerViewPager的错误,而是ViewPager内部原因导致的,可以看到很多线上的APP都存在这个问题,例如喜马拉雅(喜马拉雅的轮播图真心做的好看呀,效果也很赞!)。

这个bug虽然不影响使用,但是总感觉效果不太好。因此还是要处理一下。处理之前先分析一下问题原因。

在ViewPager内部有一个私有成员变量mFirstLayout,其默认值是true。这个参数标记为第一次布局的。如果是第一次布局那么滑动就不是平滑的。

代码如下:

public void setCurrentItem(int item) {mPopulatePending = false;setCurrentItemInternal(item, !mFirstLayout, false);
}void setCurrentItemInternal(int item, boolean smoothScroll, boolean always) {setCurrentItemInternal(item, smoothScroll, always, 0);
}

这个参数在onLayout方法中会被置为false。代码如下:

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {//  在onLayout的最后一行mFirstLayout = false;
}

因此,在正常情况下,onLayout执行之后页面滑动都应该是smooth的。然后,当ViewPager滑动出屏幕的时候其在DetachedFromWindow方法会被调用,而当其再次进入屏幕的时候则调用到AttachedToWindow这个方法。

来看看onAttachedToWindow方法中的代码:

@Override
protected void onAttachedToWindow() {super.onAttachedToWindow();mFirstLayout = true;
}

只是把mFirstLayout放置为true!而如果此时onLayout没有被触发,则先发生了页面滚动,那么此时的页面滑动就没了的平滑效果了。

了解了原因之后处理起来就简单了,因为mFirstLayout是专有属性,我们无法访问,所以只能通过反射来修改其值。我们在CatchViewPager(继承自ViewPager的一个类)中做如下操作:


private boolean firstLayout = true;@Overrideprotected void onAttachedToWindow() {super.onAttachedToWindow();hookFirstLayout();}@Overrideprotected void onDetachedFromWindow() {super.onDetachedFromWindow();firstLayout = false;}private void hookFirstLayout() {try {Field mFirstLayout = ViewPager.class.getDeclaredField("mFirstLayout");mFirstLayout.setAccessible(true);mFirstLayout.set(this, firstLayout);setCurrentItem(getCurrentItem());} catch (IllegalAccessException | NoSuchFieldException e) {e.printStackTrace();}}

这样问题就迎刃而解了,再次滑动RecyclerView,一切完好!

打造一个丝滑般自动轮播无限循环Android库相关推荐

  1. 广告栏(自动轮播,无限循环)-图片轮播控件Banner的简单使用总结

    Gradle dependencies{compile 'com.youth.banner:banner:1.1.5' //指定版本compile 'com.youth.banner:banner:+ ...

  2. vue可视化拖拽组件模板_基于 Vue 丝滑般拖拽排序组件VueSlicksort

    今天给大家分享一个功能超强的自由拖拽排序组件VueSlicksort. vue-slicksort 一款功能强大的可拖拽的vue.js组件.拥有丝滑般拖拽动画效果,支持水平/垂直/网格拖拽排序.还可以 ...

  3. 揭秘双11丝滑般剁手之路背后的网络监控技术

    简介:本篇将重点介绍Hologres在阿里巴巴网络监控部门成功替换Druid的最佳实践,并助力双11实时网络监控大盘毫秒级响应. 概要:刚刚结束的2020天猫双11中,MaxCompute交互式分析( ...

  4. xp精简工具_Windows10你也可以精简优化,丝滑般极爽轻松做到,再也不卡了

    Windows10大家也比较熟悉了,都在使用,有的喜欢有的厌恶,喜欢的肯定是非常熟练操作Windows10了,厌恶呢一般是不了解Windows10操作,不想去接触新生事物,Windows7都已经够用了 ...

  5. 让代码丝滑般跳转,rust-analyzer,你值得拥有

    1 RLS触怒了我 我是一个专一的人,从学习Rust起就在vscode中使用rls作为跳转插件(主要原因其实是懒),如果不是今天它彻底触怒了我,恐怕我还会对它继续钟情下去. 事情的原委是这样的,今天下 ...

  6. 【使用篇】WebView 实现嵌套滑动,丝滑般实现吸顶效果,完美兼容 X5 webview

    本文首发我的公众号徐公,收录于 Github·AndroidGuide,这里有 Android 进阶成长知识体系, 希望我们能够一起学习进步,关注公众号徐公,5 年中大厂程序员,一起建立核心竞争力 背 ...

  7. ant vue 树形菜单横向显示_丝滑般 Vue 拖拽排序树形表格组件Vue-DragTreeTable

    今天给小伙伴们分享一款纵享丝滑般体验的Vue拖拽树形表格DragTreeTable. vue-drag-tree-table 基于vue.js实现可拖拽排序的树形表格组件.支持拖拽排序.固定表头.拖拽 ...

  8. 深入浅出讲解丝滑般动画特效实现原理

    作者丨哈哈将 个推安卓高级开发工程师 来源丨个推技术学院 (getuitech) 前言 APP开发市场已经告别"野蛮生长"时代,人们不再满足于APP外形创新,而将目光转向全方面的用 ...

  9. android 揭示动画,Android进阶设计 | 使用揭露动画(Reveal Effect)做一个丝滑的Activity转场动画...

    提笔之际(附总体思路) 最近跟几个小伙伴在实践一个项目,考虑到界面效果,我们决定使用揭露动画作为Activity的转场动画. 这里主要是我负责这部分的实现. 话说之前是没接触过的,关于具体的实现跟大体 ...

最新文章

  1. 博为峰Java技术题 ——JavaSE Java 方法Ⅲ
  2. Java——线程安全的集合
  3. AtCoder4515 [AGC030F] Permutation and Minimum(dp)
  4. BMVC 2020 各奖项公布!最佳论文可能就是你要的涨点神器
  5. 这个“猫窝”太豪华?硅谷宠物猫住1500美元公寓
  6. python短视频教程_Python技巧:10万+的短视频被批量生产了,Python表示不服
  7. win10专业版没有触摸板选项_触摸板不适用于Windows10的解决技巧
  8. 通过 Bitmap Font Generator 生成 fnt 与 png 文件供 cocos2d-x 中 LabelBMFont 使用达到以图片表现数字
  9. 电力软件系统测试报告,电力巡检系统测试报告-软件工程
  10. 干货丨爱奇艺CDN IPv6系统配置
  11. Jeston NX ubuntu 搜狗拼音输入法安装
  12. 圆周率:山颠一寺一壶酒
  13. android 6gb和8gb区别,6GB和8GB区别到底有多大?千万别再花冤枉钱了
  14. 基于FME的地形图图面压盖检查工具的设计与制作
  15. UI设计中按钮如何设计,常见的按钮设计类型
  16. 薪酬激励的中国困境 穆穆-movno1
  17. 基于RFM模型对借贷App用户分层分析案例
  18. 【C语言】初识C语言——开端
  19. linux对4T硬盘进行分区
  20. 第四方支付平台有哪些?

热门文章

  1. malloc申请内存空间失败
  2. 信息资源管理3500字超详细,全网最全笔记!!(第一章 1)
  3. CSDN 编程挑战 博弈游戏2 斐波那契
  4. 概率统计Python计算:自定义离散型分布
  5. 阿里nlp算法实习记录
  6. 2016 HCTF web writeup
  7. 泛微OA E9后端环境搭建(IDEA) Ecology 9二次开发环境搭建 ecology二次开发
  8. 科技文献检索(十一)——常用文摘型数据库
  9. python小论文范文3000字_完整的论文范文3000字
  10. 内核小碎碎-第四集 解析dtb