文章

UI面试题

UI面试题

Fragment

Fragment 的生命周期?

Fragment 原理?

滑动

Scroller

Scroller 如何实现 View 的弹性滑动的?

  1. 在 ACTION_UP 事件触发时调用 startScroll() 方法,该方法并没有进行实际的滑动操作,而是记录滑动相关变量(滑动距离、滑动时间)
  2. 接着调用 invalidate 方法,请求 View 重绘,导致 View.draw 方法被执行
  3. 当 View 重绘后会在 draw 方法中调用 computeScroll() 方法,而 computeScroll() 又会去向 Scroller 获取当前的 scrollX 和 scrollY,然后通过 scrollTo 方法实现滑动
  4. 接着又调用 invalidate() 方法进行第二次重绘,和之前流程一样,如此反复导致 View 不断进行小幅度的滑动,而多次的小幅度滑动就组成了弹性滑动,直到整个滑动过程结束

面试题

RecyclerView

RecyclerView 卡顿

卡顿的原因:

  1. 界面设计不合理,布局层级嵌套太多,过度绘制
  2. bindViewHolder 中业务逻辑复杂,数据计算和类型转换等耗时
  3. 界面数据改变,一味地全局刷新,导致闪屏卡顿
  4. 快速滑动列表,数据加载缓慢

优化方案:

  1. 布局、绘制优化

官方建议布局层级不要超过 10 层;ViewStub 延迟加载、merge 标签;重叠 UI 移除底层背景减少过度绘制

  1. 视图绑定与数据处理分离

在 onBindViewHolder 进行日期比较和日期格式化是很耗时的,onBindViewHolder 方法中应该只将数据显示到视图上,而不应该进行业务的处理,业务处理在之前处理

  1. notifyxxx() 局部刷新,payloads
  2. 改变 mCachedViews 的缓存,默认为 2,这里面的 ViewHolder 缓存数据也在,根据 position 复用,不需要重新绑定 onBindViewHolder()

通过 setViewCacheSize(int) 方法增大缓存的大小;用空间换时间

  1. 共享 RecycledViewPool
  2. 惯性滑动延迟加载

监听 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. 递归实现
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)
        }
    }
}
  1. 广度优先实现

广度优先的过程,就是对每一层节点依次访问,访问完了再进入下一层。就是按树的深度,一层层的遍历访问;广度优先非常适合用先入先出的队列来实现,每次子 View 都入队尾,而从对头取新的 View 进行处理。

ftmo3

2xm9r

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. 深度优先实现

深度优先的过程,就是对每个可能的分支路径,深度到叶子节点,并且每个节点只访问一次。深度优先非常适合用先入后出的栈来实现

nel8t

srha1

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) 滑动冲突的问题
p7mwy

这个图中顶部是一个 ImageView,下面是整体是一个 RecyclerView,其中包含横向可滑动的 RecyclerView),如图所示滑动下面的 item 时候,顶部的 ImageView 并不会自动折叠

原因: CoordinatorLayout 实现了 NestedScrollingParent2, 外层的 RecyclerView 是 CoordinatorLayout 的子类,滑动的时候会通知 CoordinatorLayout,进而由其协调 CollapsingToolbarLayout 发生折叠。而内部嵌套的横向 RecyclerView 只是实现了 NestedScrollingChild2, 属于外层 RecyclerView 的子类, 如果不关闭横向滑动的嵌套滑动功能,就不能像其它纵向嵌入的 View 一样触发折叠
解决:
对内部嵌套的横向滑动的 RecyclerView 设置它的 setNestedScrollingEnabled(false) 即可

本文由作者按照 CC BY 4.0 进行授权