欠大家一篇文章,这段时间事情较多常在外面跑来跑去的,其本上没什么时间静下来写代码。然后看到了不少的网友给我反馈,想看一下是如何实现这个效果的:

原面试题传送门。

我看到有人通过RecyclerView来实现这个效果,其实也可以,只是背离了考查自定义UI开发的目的。这里我做了一个简单的实现,当然是不完整的,我希望大家自己动手来完善它,那样这个实例中涉及的知识才能真正转化成你自己的技能。

这里简单说一下实现步骤:

实现自定义的圆角带阴影的View

实现GroupView+Adapter完成布局

实现滑动

源码下载地址:Github

实现圆角带阴影的ImaegView

之前我们也有提过有几种实现图片圆角的方式,这里我们只展示一下使用BitmapShader的方式。我们使用paint.setShader设置画笔的内容,再用drawCircle通过设定好的paint画圆即可实现图片的圆角效果。在真正偿试时,你可能需要先解决如下的问题:

canvas的坐标系和drawCircle的坐标关系。

public void drawCircle(float cx, float cy, float radius, @NonNull Paint paint)

了解Shader和BitmapShader。

new BitmapShader(imageBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);

BitmapShader的三种TileMode: CLAMP, REPEAT, MIRROR

了解一些Matrix的基础知识。

学会让Matrix进行缩放让图片居中显示。

了解关于Paint的一些用法。

学会给paint添加阴影效果:

public void setShadowLayer(float radius, float dx, float dy, int shadowColor)

这里不需要像面试一样问你Canvas、Drawable和Bitmap之间的关系,因为当你动手实现完这这个实例时,你自然会明白他们之间的关联。

实现GroupView+Adapter完成布局

onLayout在ViewGroup中是一个抽象方法,所以你自定义ViewGroup的话首先要重写它的一个实现。

我们需要给每一个可显示的子View设置它的显示位置(确定它四个点的位置):

public void layout(int l, int t, int r, int b)

在这个onLayout中我们先用最简单的线性布局一行排开所有的子View:

@Override

protected void onLayout(boolean changed, int l, int t, int r, int b) {
int viewGroupWidth = getMeasuredWidth();
int painterPosX = l;
int painterPosY = t;
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
View childView = getChildAt(i);
int width = childView.getMeasuredWidth();
int height = childView.getMeasuredHeight();
childView.layout(painterPosX, painterPosY, painterPosX + width, painterPosY + height);
painterPosX += width;
}
}

验证onLayout如保布局,我们第一步是给ViewGroup添加子View,这里我们先用一种最简单的方式,在xml文件中直接添加:

android:layout_width="match_parent"
android:layout_height="wrap_content">
android:layout_width="96dp"
android:layout_height="96dp"
android:src="@mipmap/goeasyway"/>
android:layout_width="96dp"
android:layout_height="96dp"
android:src="@mipmap/goeasyway"/>
android:layout_width="96dp"
android:layout_height="96dp"
android:src="@mipmap/goeasyway"/>
android:layout_width="96dp"
android:layout_height="96dp"
android:src="@mipmap/goeasyway"/>
android:layout_width="96dp"
android:layout_height="96dp"
android:src="@mipmap/goeasyway"/>

我们在CoverflowView下添加了5个CircularImageView的子View,在屏幕上看只能显示下4个:

当然,现在我们希望通过滑动能看到第5个子View。

实现滑动

其实滑动和动画效果的原理是一样的,就是让子View在短时间内连续地改变坐位或者大小(当然还有颜色等其他因素,也会产生动态的效果)。一秒内能显示的连续改变越多效果也就是越流畅(有点象录音的采样率)。

所以,在动手前你还需要检查一下下面罗列的概念或方法你是否有不明白的,可以在动手前稍稍了解一下:

获得滑动输入:onInterceptTouchEvent、onTouchEvent

计算:computeScroll、scrollBy & scrollTo、Scroller

调用View的绘制流程更新View:onMeasure、onLayout、onDraw

之后的问题会集中在怎么确定选中的Item,所以你会比较清楚一些常用到的坐标值各表示什么意思,如getX(), getRawX(), getScrollX()。

最后按这个面试题的要求添加一些功能:非选中的图像会缩小并且半透明显示,添加上Adapter让滑动人Item可以按需求增减。

最终我们实现一个类似的效果:

问题和优化

这是一个简单的实现方式,为什么要写一个简单的实现呢?我认为这样对于自己梳理知识(写文章)和对于想学习UI开发的朋友都是最有利的,我们先动手,然后发现我们遇到的问题,然后把这些问题解决了,然后获得提高!

Adapter添加上的子View如何计算高度和宽度?

本实例中我们写死了子View的大小。

可以添加fling实现更好的滑动体验。

了解一些概念:VelocityTracker & Scroller.fling

了解addView & addViewInLayout的区别,我们还没有处理View的缓存的问题。

不需要每次都把所有的子View添加到ViewGroup,而只添加在可视区域能界能出来的View。当子View滑出ViewGroup的可视区域时,如何处理?同理,刚滑进可视区时如何处理?

如何通知外部选中项发生了变化?

这个问题应该不对实现吧。

这个实现还有很多可以优化的地方, 比如性能上,现在可能感觉不出来,但是当我们的Item比较复杂时(现在我们只是一个ImageView),而且Adapter中的数据较多时,就有可能会出现明显的卡顿现象。像不像ListView遇到的问题,其实是一样的,希望大家动手试试。

最后

有更好实现的,期待大家给我投稿,之后有时间我会再公布一版优化的版本。

未完待大家一起动手......