前言部分
因为项目要对接猫眼电影,需要做一个和猫眼电影一样的影片选择效果,其实这本篇文章的内容是网上凑出来的。虽说是东拼西凑,但是过程中还是有些收获,毕竟有些东西还是要亲自实践才能深入理解。
不多说先上个效果图吧:
内容部分
实现效果大体分为几部分:
- 一个横向滚动的列表(RecyclerView)
- 列表要选中的条目需要居中显示(LinearSnapHelper)
- 选中的条目需要放大效果(或者把未选中的缩小)
- 添加一个背景高斯模糊,这个网上有标准写法,直接拿来使用,未考虑性能等问题。
- 细节上就是打开页面播放了一个动画,让item有一个类似呼吸的效果。
下面正式开始实现
-
首先一个横向的RecyclerView列表先实现出来。这个很常规就不贴代码了。
-
让条目居中这里我们使用到一个辅助的类LinearSnapHelper,这个类其实很早就有了,但是并没有很大的被使用,我也是上次用RecyclerView实现viewpager效果的时候使用过PagerSnapHelper。代码并不需要我们做特别的处理,主要调用attachToRecyclerView方法传入RecyclerView即可。
-
条目的放大特效实现,这里我自己写了一个,但是至少单纯调用view的setScaleX方法,效果看起来比较僵硬,后来又使用动画实现了一版改良效果。但始终还是不尽如人意,后来就网上搜索了一个方案,通过判断选中的view距离中心点的距离进行缩放。代码如下:
@Override public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, RecyclerView.State state) { int childCount = mRecyclerView.getChildCount(); // Log.e("scrollHorizontallyBy", dx + ""); int[] location = new int[2]; for (int i = 0; i < childCount; i++) { View v = mRecyclerView.getChildAt(i); v.getLocationOnScreen(location); int recyclerViewCenterX = mRecyclerView.getLeft() + mRecyclerView.getWidth() / 2; int itemCenterX = location[0] + v.getWidth() / 2; // ★ 两边的图片缩放比例 float scale = 0.8f; // ★某个item中心X坐标距recyclerview中心X坐标的偏移量 int offX = Math.abs(itemCenterX - recyclerViewCenterX); // ★ 在一个item的宽度范围内,item从1缩放至scale,那么改变了(1-scale), // 从下列公式算出随着offX变化,item的变化缩放百分比 float percent = offX * (1 - scale) / v.getWidth(); // ★ 取反哟 float interpretateScale = 1 - percent; // 这个if不走的话,得到的是多级渐变模式 if (interpretateScale < scale) { interpretateScale = scale; } v.setScaleX((interpretateScale)); v.setScaleY((interpretateScale)); } return super.scrollHorizontallyBy(dx, recycler, state); }
-
高斯模糊的实现也是网上摘了一段代码,这里需要注意的可能是你需要通过每个条目上的图片来做模糊,所以生成的模糊后的图片尺寸要设置尽可能小,因为bitmap的操作是很消耗内存的,所以要满足效果的同时尽可能减少内存的消耗。代码如下:
public void onScrollStateChanged(int state) { switch (state) { case RecyclerView.SCROLL_STATE_IDLE: View snapView = linearSnapHelper.findSnapView(this); ImageView image = snapView.findViewById(R.id.image); Drawable background = image.getDrawable(); mRecyclerView.setBackground(creatBitmap(Utils.drawableToBitmap(background))); currentPosition = getPosition(snapView); if (positionSelectListener != null) { if (oldPosition != currentPosition) { positionSelectListener.selectItem(currentPosition); oldPosition = currentPosition; } } break; case RecyclerView.SCROLL_STATE_DRAGGING: break; case RecyclerView.SCROLL_STATE_SETTLING: break; default: break; } }
上面主要过程是,通过linearSnapHelper.findSnapView(this)来获取中心的view,然后在view中获取到需要做模糊处理的image,然后在依次进行转换Drawable—>Bitmap—>转换完成的bitmap—>Drawable—>设置给mRecyclerView的背景。
下面类似的是一个工具方法了,详细的内容不在多说了,因为我也不了解。哈哈
private Drawable creatBitmap(Bitmap bitmap) { //创建一个缩小后的bitmap Bitmap inputBitmap = Bitmap.createScaledBitmap(bitmap, 10, 10, false); //创建将在ondraw中使用到的经过模糊处理后的bitmap Bitmap outputBitmap = Bitmap.createBitmap(inputBitmap); //创建RenderScript,ScriptIntrinsicBlur固定写法 RenderScript rs = RenderScript.create(context); ScriptIntrinsicBlur blurScript = ScriptIntrinsicBlur.create(rs, Element.U8_4(rs)); //根据inputBitmap,outputBitmap分别分配内存 Allocation tmpIn = Allocation.createFromBitmap(rs, inputBitmap); Allocation tmpOut = Allocation.createFromBitmap(rs, outputBitmap); //设置模糊半径取值0-25之间,不同半径得到的模糊效果不同 blurScript.setRadius(15); blurScript.setInput(tmpIn); blurScript.forEach(tmpOut); //得到最终的模糊bitmap tmpOut.copyTo(outputBitmap); return new BitmapDrawable(outputBitmap); }
-
初始化的动画我加在了onLayoutCompleted方法中并且做了一些限制,因为该方法在一些情况下要被多次调用。下面是代码:
if (isFirstCompleted) { isFirstCompleted = false; childCount = mRecyclerView.getChildCount(); mRecyclerView.setEnabled(false); for (int i = 0; i < childCount; i++) { View v = mRecyclerView.getChildAt(i); animatorSet = new AnimatorSet(); ObjectAnimator scaleX = ObjectAnimator.ofFloat(v, "scaleX", 1.0f, 1.1f, 0.9f, 0.8f); ObjectAnimator scaleY = ObjectAnimator.ofFloat(v, "scaleY", 1.0f, 1.1f, 0.9f, 0.8f); animatorSet.setDuration(1000); animatorSet.setInterpolator(new LinearInterpolator()); animatorSet.play(scaleX).with(scaleY); animatorSet.start(); } animatorSet.addListener(new SampleAnimatorListenerImpl() { @Override public void onAnimationEnd(Animator animation) { //初始化完成,first上的position放大 View v = mRecyclerView.getChildAt(0); AnimatorSet animatorSet = new AnimatorSet(); ObjectAnimator scaleX = ObjectAnimator.ofFloat(v, "scaleX", 0.8f, 1.0f); ObjectAnimator scaleY = ObjectAnimator.ofFloat(v, "scaleY", 0.8f, 1.0f); animatorSet.setDuration(300); animatorSet.setInterpolator(new LinearInterpolator()); animatorSet.play(scaleX).with(scaleY); animatorSet.start(); ImageView image = v.findViewById(R.id.image); Drawable background = image.getDrawable(); mRecyclerView.setBackground(creatBitmap(Utils.drawableToBitmap(background))); canScroll = true; } }); }
上面主要是两个部分,一部分是给整体显示的item设置一个呼吸的动画效果;一部分是在动画完成后给第一个选中的item设置一个放大的效果和一个背景模糊的效果。
-
处理一下调用mRecyclerview.smoothScrollToPosition(position);方法的逻辑,源码显示调用该方法后会调用到layoutManager中的smoothScrollToPosition()方法,在这我们要处理一下中心对齐。代码如下:
CenterSmoothScroller centerSmoothScroller = new CenterSmoothScroller(context); centerSmoothScroller.setTargetPosition(position); startSmoothScroll(centerSmoothScroller); //CenterSmoothScroller类中主要就是方法如下,其中对位置进行了修正。这里没读源码有兴趣可以了解一下LinearSmoothScroller类。 @Override public int calculateDtToFit(int viewStart, int viewEnd, int boxStart, int boxEnd, int snapPreference) { return boxStart + (boxEnd - boxStart) / 2 - (viewStart + (viewEnd - viewStart) / 2); }
-
添加一个回调方法告诉调用者当前是选中的哪个position,调用者对应更新影片详情。这里就不具体说了,比较简单。
结束部分
其实上面的内容并没有我自己写的东西,不过是把网上的东西东拼西凑出来的,但是在拼凑前确实也仔细思考实现,只是效果不如人意,其实其他东西也是可以先自己来思考处理实现,然后在去看轮子如何实现,从中找到差异来不断精进,毕竟不是每个人都是大神。