반사 | 안드로이드보기 메커니즘 설계 및 구현 : 레이아웃 프로세스

반사 시리즈 블로그는 새로운 방법을 배울 수있는 내 시도, 내용의 시리즈와 테이블의 기원은, 참조하십시오 여기 .

개요

Android자신의 View시스템이 매우 큰 소스 코드 가치가 생각하고 많은 배우 View의를 통해, 예를 들어, 프로세스 자체를 그릴 measure측정, layout레이아웃 draw세 가지 프로세스를 그리기, 궁극적으로 사용자의 앞에 그것을 끌어와 설명 할 수.

비교 과정을 측정 , 레이아웃 과정은 독자가 이해하지 않는 경우, 상대적으로 훨씬 간단 측정 과정을 , 나는이 기사를 읽어 보시기 바랍니다 :

반사 | 안드로이드보기 메커니즘의 설계 및 구현 : 측정 프로세스

전체 아이디어

측정 유량 을 목적으로한다 너비와 높이 제어를 측정 ,하지만 사실 컨트롤의 폭과 높이를 얻는 것은 충분하지 않습니다에 대한 ViewGroup목적은 로직의 추가 설정이 필요, 그 자식 컨트롤 모두에 해당하는 정책의 레이아웃에 대한 책임, 이것은이다 레이아웃 과정 ( 레이아웃).

  • 리프 노드의 경우 1. View그는 컨트롤의 논리적 서브 레이아웃의 처리를 필요로하지 않는, 자식 컨트롤을 가지고, 따라서 일반적으로 단지 부모 컨트롤에 자신의 위치를 기록 할 필요가 없습니다;
  • 하여 프로세스의 전체 레이아웃, 반드시 부모 컨트롤에 넘겨 자식 제어 장치의 위치, 그리고 2. 측정 과정 으로, Android레이아웃 과정은 또한 재귀 아이디어를 사용에서 : 완전한 인터페이스의 경우, 각 페이지 매핑 View는 트리 구조 제어를 보장하기 위해 다음 사항을 언급 할 가치가있다 - 나무, 부모 컨트롤의 위쪽은 차례로 자신의 유통 전략을 통해 각 자식 컨트롤의 위치를 계산, 레이아웃으로 시작 내부 자율성 각 자식 컨트롤의를 같은 위치 부모 제어의 좌표계에 대한 상대 위치 가 아니라 절대 위치 중 화면 좌표보다. 위치를 계산 한 후, 하위 레이아웃을 제어 시작되도록 하위 제어 파라미터로서 등등의 컨트롤의 바닥까지 때 완료 레이아웃 모든 제어, 전체 배치 프로세스 종료한다.

캐릭터의 측면에서 개발자 익숙하지 않은 레이아웃 프로세스에 대한 모호한 것 같다,하지만 설계 과정의 전체 레이아웃, 이들 문자의 본질을 요약 독자가 소스 코드 분석으로 해석하지 말아야하며, 자신에 대입한다 설계 과정에 깊은 디자인 아이디어, 레이아웃, 공정 설계의 전 과정의 이해와 하나가 자연의 이동에서의 격차를 채우기 위해 코드를 작성.

하나의 레이아웃 과정을보기

첫째, 최대 해당 위치에 대한 각 자식 컨트롤을 할당 문제, 측정 종료 후 레이아웃 과정의 본질에 대해 생각 - 자식 컨트롤이 있기 때문에, 그것은 메인 레이아웃 과정이 있어야 보여줍니다 ViewGroup하나의 리프 노드로 다음 View의 왜 레이아웃 과정이있을 것입니다?

독자 중시을 그릴 수 있고, 레이아웃 프로세스가 실제로 복잡한 과정이며, 다음과 같이 기본 프로세스의 전체 논리 시퀀스이다 :

  • 당신이 재 측정 과정에 필요 여부를 결정하십시오 onMeasure();
  • 2. 정보 자체가 저장되는 위치;
  • 3. 레이아웃은 개시제의 레이아웃을 변경하는 프로세스의 여부를 판단한다;
  • 4. 레이아웃이 변경된 경우, 그래서 모든 자식 컨트롤 레이아웃을 다시 것을;
  • 레이아웃이 변경되는 경우 5. 레이아웃 변경 알림 리스너는 전송 된 모든 알림을 관찰합니다.

전체 레이아웃 과정은 또한 4는 ViewGroup그 자체가 수행해야 할 다른위한 논리 ViewViewGroup조건은 공개됩니다 - 그 개인을 나타내는 것은 View레이아웃 과정을 필요로한다.

이제 전체 레이아웃 과정, 즉, 세 가지 중요한 기능을 정의 :

  • void layout(int l, int t, int r, int b): 자신의 기능 전체 레이아웃 프로세스를 제어;
  • void onLayout(boolean changed, int left, int top, int right, int bottom): 뷰 그룹 레이아웃 로직 기능은 개발자는 사용자 정의 레이아웃 논리를 구현해야;
  • void setFrame(int left, int top, int right, int bottom): 함수의 최신 레이아웃 위치 정보 저장;

이유는 세 가지 기능을 정의하는이 필요?

시작 플래그 레이아웃 : 1.layout 기능

이제 우리는 하나의 스탠드 View관점을 먼저 부모 컨트롤이 자식 컨트롤의 호출에 의해 요구되는 layout()자식 컨트롤의 위치 (동안, 함수를 left、right、top、bottom매개 변수로는) 레이아웃 과정 자체 자식 컨트롤의 표시를 시작합니다 :

// 伪代码实现
public void layout(int l, int t, int r, int b) {
  // 1.决定是否需要重新进行测量流程(onMeasure)
  if(needMeasureBeforeLayout) {
    onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec)
  }

  // 先将之前的位置信息进行保存
  int oldL = mLeft;
  int oldT = mTop;
  int oldB = mBottom;
  int oldR = mRight;
  // 2.将自身所在的位置信息进行保存;
  // 3.判断本次布局流程是否引发了布局的改变;
  boolean changed = setFrame(l, t, r, b);

  if (changed) {
    // 4.若布局发生了改变,令所有子控件重新布局;
    onLayout(changed, l, t, r, b);
    // 5.若布局发生了改变,通知所有观察布局改变的监听发送通知
    mOnLayoutChangeListener.onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
  }
}

여기에 의사 코드 레이아웃 과정의 설명, 사실의 방법으로 저자 View자신의 layout()- 내부 기능, 비록 여러 가지 있지만, 핵심 아이디어는 동일합니다 layout()전체 프로세스가 실제로 자신의 레이아웃을 나타내는 기능 컨트롤 setFrame()onLayout()기능은 다음 layout()에 한 단계.

2.setFrame 기능이 레이아웃 정보 저장

이유는 레이아웃 정보를 저장해야합니까? 우리는 항상 폭이 높은 수요가 컨트롤을 가져옵니다 때문에 - 예를 들면 다음과 같이 onDraw()그리기 단계; 및 레이아웃 정보 저장, 우리는 컨트롤 자체의 폭과 높이를 계산하기 위해이 값을 통과 할 수있을 것입니다 :

public final int getWidth() { return mWidth; }

public final int getHeight() { return mHeight; }

따라서, 제어 레이아웃 정보의 보존이 실제로 매우 필요하다, 안드로이드는 것 layout()네 개의 매개 변수의 위치 정보 기능이에 나타내는 setFrame()저장 기능 :

protected boolean setFrame(int left, int top, int right, int bottom) {
    // 布局是否发生了改变
    boolean changed = false;
    // 若最新的布局信息和之前的布局信息不同,则保存最新的布局信息
    if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {
        changed = true;
        mLeft = left;
        mTop = top;
        mRight = right;
        mBottom = bottom;
    }
    return changed;
}

setFrame()함수가되어 protected있는 개발자 정의하는이 기능을 대체 할 수 있음을 의미 변성 View지금까지 회전 로직의 레이아웃 정보를 저장하는 자체 mLeft、mTop、mRight、mBottom네 변수.

이름이 자연스럽게 해당,이 네 가지 변수를 암시 하듯이 View자신의 위치를,이 View위치 정보는 네 가지 변수들에 의해 설명 제어하는 방법입니까?

3. 상대 위치 및 절대 위치

지도보기로 표현이 네 가지 변수의 의미 :

이 시간은 필연적 있다는 또 다른 문제에 직면 mLeft、mTop、mRight、mBottom좌표 시스템의 대응 값이 어디 있는지?

이는 트리 컨트롤 보장하기 위해, 그 주목해야 내부 자율성 의 각 자식 컨트롤의 위치 부모 제어의 좌표계의 상대 위치 가 아니라 대상의 절대 위치를 화면 좌표 이하인 :

상기 위치 정보는 좌표 우선 화면에있는 경우, 다른 방법으로 라운드는, 그 후에는 각 리프 노드는 것을 의미 View보유 할 루트에서 저장 될 ViewGroup부모까지 ViewGroup레이아웃 계산시 각 제어 위치 정보를 더 복잡 분명히 불합리한 디자인입니다.

때문에 View이러한 위치 정보를 자신의 보유, 그 폭 및 높이의 제어가 실제로 이미 취득 getWidth()하고 getHeight()상기 방법은이 방법을 다시 정의 할 수있다 :

public final int getWidth() { return mRight - mLeft; }

public final int getHeight() { return mBottom - mTop; }

이것은 또한 레이아웃의 처리에 대해 설명 setFrame()하는 기능이 완료된 후에 실행 (및 레이아웃 변경 않음) 개발자 전달할 수 getWidth()getHeight()제어 방법의 정확한 폭과 높이의 값을 얻는다.

4.onLayout 기능 : 위치 산출 자식 컨트롤

리프 노드의 경우 View, 자식 컨트롤과하지 않았다 따라서 일반적으로하지 (특별한 경우를 참조하십시오 자식 컨트롤 레이아웃을 의미하는 것으로 AppCompatTextView, 그래서 같은 클래스로) 기능이 빈을 달성하기 위해 설계되었습니다 :ViewonLayout()

protected void onLayout(boolean changed, int left, int top, int right, int bottom) {  }

에서 ViewGroup, 다른 유형은 ViewGroup다른 레이아웃 정책이, 서로 다른 전략의 논리적 레이아웃,이 방법은 개발자가 정의하는 방법을 구현해야합니다, 추상적 인 인터페이스를 설계 ViewGroup레이아웃 정책 :

@Override
protected abstract void onLayout(boolean changed,int l, int t, int r, int b);

LinearLayout为例,其布局策略为 根据排布方向,将其所有子控件按照指定方向依次排列布局

至此单个View的测量流程结束,关于ViewGrouponLayout函数细节将在下文进行描述。

完整布局流程

相比较测量流程,布局流程相对比较简单,整体思路是,对于一个完整的界面而言,每个页面都映射了一个View树,最顶端的父控件开始布局时,会通过自身的布局策略依次计算出每个子控件的位置。位置计算完毕后,作为参数交给子控件,令子控件开始布局;如此往复一直到最底层的控件,当所有控件都布局完毕,整个布局流程结束。

ViewGroup虽然重写了Viewlayout()函数,但实质上并未进行大的变动,我们大抵可以认为ViewGroupViewlayout()逻辑一致:

@Override
public final void layout(int l, int t, int r, int b) {
    if (!mSuppressLayout && (mTransition == null || !mTransition.isChangingLayout())) {
        if (mTransition != null) {
            mTransition.layoutChange(this);
        }
        // 仍然是执行View层的layout函数
        super.layout(l, t, r, b);
    } else {
        mLayoutCalledWhileSuppressed = true;
    }
}

唯一需要注意的是,开发者必须实现onLayout()函数以定义ViewGroup的布局策略,这里以 竖直布局LinearLayout的伪代码为例:

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
  int childTop;
  int childLeft;

  // 遍历所有子View
  for (int i = 0; i < count; i++) {
    // 获取子View
    final View child = getVirtualChildAt(i);
    // 获取子View宽高,注意这里使用的是 getMeasuredWidth 而不是 getWidth
    final int childWidth = child.getMeasuredWidth();
    final int childHeight = child.getMeasuredHeight();

    // 令所有子控件开始布局
    setChildFrame(child, childLeft, childTop, childWidth, childHeight);   
    // 高度累加,下一个子View的 top 就等于上一个子View的 bottom ,符合竖直线性布局从上到下的布局策略   
    childTop += childHeight;      
  }
}

private void setChildFrame(View child, int left, int top, int width, int height) {
    // 这里可以看到,子控件的mRight实际上就是 mLeft + getMeasuredWidth()
    // 而在getWidth()函数中,mRight-mLeft的结果就是getMeasuredWidth()
    // 因此,getWidth() 和 getMeasuredWidth() 是一致的
    child.layout(left, top, left + width, top + height);
}

读者需要注意到一个细节,子控件的宽度的获取,我们并未使用getWidth(),而是使用了getMeasuredWidth(),这就引发了另外一个疑问,这两个函数的区别在哪里。

getWidth 和 getMeasuredWidth 的区别

首先,从上文中我们得知,getWidth()getHeight()函数的相关信息实际上是在setFrame()函数执行完毕才准备完毕的——我们大致可以认为是这两个函数 只有布局流程(layout)执行完毕才能调用,而在父控件的onLayout()函数中,获取子控件宽度和高度时,子控件还并未开始进行布局流程,因此此时不能调用getWidth()函数,而只能通过getMeasuredWidth()函数获取控件测量阶段结果的宽度。

那么当控件绘制流程执行完毕后,getWidth()getMeasuredWidth()函数的值有什么区别呢?从上述setChildFrame()函数中的源码可以得知,布局流程执行后,getWidth()返回值的本质其实就是getMeasuredWidth()——因此本质上,当我们没有手动调用layout()函数强制修改控件的布局信息的话,两个函数的返回值大小是完全一致的。

整体流程小结

在整个布局流程的设计中,设计者将流程中公共的业务逻辑(保存布局信息、通知布局发生改变的监听等)通过layout()函数进行了整合,同时,将ViewGroup额外需要的自定义布局策略通过onLayout()函数向外暴露出来,针对组件中代码的可复用性和可扩展性进行了合理的设计。

至此,布局流程整体实现完毕。借用 carson_ho 绘制的流程图对整体布局流程做一个总结:

参考


关于我

Hello,我是 却把清梅嗅 ,如果您觉得文章对您有价值,欢迎 ❤️,也欢迎关注我的 博客 或者 Github

如果您觉得文章还差了那么点东西,也请通过关注督促我写出更好的文章——万一哪天我进步了呢?

发布了99 篇原创文章 · 获赞 396 · 访问量 47万+

추천

출처blog.csdn.net/mq2553299/article/details/99457371