UI面试题
Fragment
Fragment 的生命周期?
Fragment 原理?
滑动
Scroller
Scroller 如何实现 View 的弹性滑动的?
- 在 ACTION_UP 事件触发时调用 startScroll() 方法,该方法并没有进行实际的滑动操作,而是记录滑动相关变量(滑动距离、滑动时间)
- 接着调用 invalidate 方法,请求 View 重绘,导致 View.draw 方法被执行
- 当 View 重绘后会在 draw 方法中调用 computeScroll() 方法,而 computeScroll() 又会去向 Scroller 获取当前的 scrollX 和 scrollY,然后通过 scrollTo 方法实现滑动
- 接着又调用 invalidate() 方法进行第二次重绘,和之前流程一样,如此反复导致 View 不断进行小幅度的滑动,而多次的小幅度滑动就组成了弹性滑动,直到整个滑动过程结束
面试题
RecyclerView
RecyclerView 卡顿
卡顿的原因:
- 界面设计不合理,布局层级嵌套太多,过度绘制
- bindViewHolder 中业务逻辑复杂,数据计算和类型转换等耗时
- 界面数据改变,一味地全局刷新,导致闪屏卡顿
- 快速滑动列表,数据加载缓慢
优化方案:
- 布局、绘制优化
官方建议布局层级不要超过 10 层;ViewStub 延迟加载、merge 标签;重叠 UI 移除底层背景减少过度绘制
- 视图绑定与数据处理分离
在 onBindViewHolder 进行日期比较和日期格式化是很耗时的,onBindViewHolder 方法中应该只将数据显示到视图上,而不应该进行业务的处理,业务处理在之前处理
- notifyxxx() 局部刷新,payloads
- 改变 mCachedViews 的缓存,默认为 2,这里面的 ViewHolder 缓存数据也在,根据 position 复用,不需要重新绑定 onBindViewHolder()
通过
setViewCacheSize(int)
方法增大缓存的大小;用空间换时间
- 共享 RecycledViewPool
- 惯性滑动延迟加载
监听 addOnScrollListener,在滑动过程中不加载,滚动静止时,刷新界面,实现加载
如何让 RecycleView 的 item 不被 detach (抖音)
TextureView 在 rv 中不闪屏 (抖音)
UI 场景题?
自定义一个五角星 View?如果需要动的话?
如何快速镜像(翻转)一个 View?
方式 1: 推荐,不需要重写 onInterceptTouchEvent 方法
android: scaleX = -1
可用来水平镜像翻转android: scaleY = -1
可用来垂直镜像翻转
方式 2: RotateAnimation,需翻转事件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class Rotate3dAnimation extends Animation {
// 中心点
private final float mCenterX;
private final float mCenterY;
// 3D变换处理camera(不是摄像头)
private Camera mCamera = new Camera();
/**
* @param centerX 翻转中心x坐标
* @param centerY 翻转中心y坐标
*/
public Rotate3dAnimation(float centerX,
float centerY) {
mCenterX = centerX;
mCenterY = centerY;
}
@Override
protected void applyTransformation(float interpolatedTime, Transformation t) {
// 生成中间角度
final Camera camera = mCamera;
final Matrix matrix = t.getMatrix();
camera.save();
camera.rotateY(180);
// 取得变换后的矩阵
camera.getMatrix(matrix);
camera.restore();
matrix.preTranslate(-mCenterX, -mCenterY);
matrix.postTranslate(mCenterX, mCenterY);
}
}
仅仅是实现了显示的翻转,手势操作的位置并没有发生翻转,手势翻转需要重写外层的 viewgroup 的 onInterceptTouchEvent 方法,对下发的 MotionEvent 进行一次翻转操作,使得 childView 接收到的手势都是反过来的。
方式 3: Canvas.scale(-1, 1, xxx, xxx) ,需翻转事件
- 普通 View
- ViewGroup 在 dispatchDraw 中进行 canvas scale
- View 在 onDraw 中进行 canvas scale
- SurfaceView
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class TestSurfaceView extends SurfaceView implements SurfaceHolder.Callback{
SurfaceHolder surfaceHolder ;
public TestSurfaceView(Context context, AttributeSet attrs) {
super(context, attrs);
surfaceHolder = this.getHolder();
surfaceHolder.addCallback(this);
}
@Override
public void surfaceCreated(SurfaceHolder holder) {
Canvas canvas = surfaceHolder.lockCanvas();
//绘制之前先对画布进行翻转
canvas.scale(-1,1, getWidth()/2,getHeight()/2);
//开始自己的内容的绘制
Paint paint = new Paint();
canvas.drawColor(Color.WHITE);
paint.setColor(Color.BLACK);
paint.setTextSize(50);
canvas.drawText("这是对SurfaceView的翻转",50,250,paint);
surfaceHolder.unlockCanvasAndPost(canvas);
}
}
其他
View 树遍历是深度还是广度优先?
Android 对 View 树进行布局时,采用的是深度优先算法,遍历到某个 View 时,首先沿着该 View 一直纵向遍历并布局到处于叶子节点的 View,只有对该 View 及其所有子孙 View 完成布局后,才会布局该 View 的兄弟节点 View
给定 ViewGroup 打印其内所有的 View
- 递归实现
1
2
3
4
5
6
7
8
9
fun recursionPrint(root: View) {
printView(root)
if (root is ViewGroup) {
for (childIndex in 0 until root.childCount) {
val childView = root.getChildAt(childIndex)
recursionPrint(childView)
}
}
}
- 广度优先实现
广度优先的过程,就是对每一层节点依次访问,访问完了再进入下一层。就是按树的深度,一层层的遍历访问;广度优先非常适合用先入先出的队列来实现,每次子 View 都入队尾,而从对头取新的 View 进行处理。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
fun breadthFirst(root :View){
val viewDeque = LinkedList<View>()
var view = root
viewDeque.push(view)
while (!viewDeque.isEmpty()){
view = viewDeque.poll()
printView(view) // 这里是打印
if(view is ViewGroup){
for(childIndex in 0 until view.childCount){
val childView = view.getChildAt(childIndex)
viewDeque.addLast(childView)
}
}
}
}
- 深度优先实现
深度优先的过程,就是对每个可能的分支路径,深度到叶子节点,并且每个节点只访问一次。深度优先非常适合用先入后出的栈来实现。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
fun depthFirst(root :View){
val viewDeque = LinkedList<View>()
var view = root
viewDeque.push(view)
while (!viewDeque.isEmpty()){
view = viewDeque.pop()
printView(view)
if(view is ViewGroup){
for(childIndex in 0 until view.childCount){
val childView = view.getChildAt(childIndex)
viewDeque.push(childView)
}
}
}
}
变种题: 统计 ViewGroup 子 View 的数量、分层打印 ViewTree、查找 ID 为 Xxx 的 View 等,
CoordinatorLayout 事件冲突怎么解决?
AppBarLayout+CoordinatorLayout+RecyclerView(其中包含横向滑动的 RecyclerView) 滑动冲突的问题
这个图中顶部是一个 ImageView,下面是整体是一个 RecyclerView,其中包含横向可滑动的 RecyclerView),如图所示滑动下面的 item 时候,顶部的 ImageView 并不会自动折叠
原因: CoordinatorLayout 实现了 NestedScrollingParent2, 外层的 RecyclerView 是 CoordinatorLayout 的子类,滑动的时候会通知 CoordinatorLayout,进而由其协调 CollapsingToolbarLayout 发生折叠。而内部嵌套的横向 RecyclerView 只是实现了 NestedScrollingChild2, 属于外层 RecyclerView 的子类, 如果不关闭横向滑动的嵌套滑动功能,就不能像其它纵向嵌入的 View 一样触发折叠
解决:
对内部嵌套的横向滑动的 RecyclerView 设置它的 setNestedScrollingEnabled(false) 即可