O framework do Android solicita que uma
Activity
desenhe o layout quando a
Activity
recebe foco. Ele gerencia o procedimento do desenho, mas a
Activity
precisa fornecer
o nó raiz da hierarquia de layout.
O framework do Android desenha o nó raiz do layout e mede a árvore de layout. Ela
desenha percorrendo a árvore e renderizando cada
View
que cruza a região inválida.
Cada ViewGroup
é responsável por
solicitar que cada filho seja desenhado, usando o método
draw()
,
e cada View
é responsável por desenhar a si mesmo. Como a árvore é percorrida
em uma pré-ordem, o framework desenha os pais antes (ou seja, atrás)
dos filhos e os irmãos na ordem em que aparecem na árvore.
O framework do Android desenha o layout em um processo de duas etapas: uma passagem de medição e uma de layout. O
framework realiza a passagem de medição em
measure(int, int)
e
uma passagem de cima para baixo da árvore de View
. Cada View
envia as especificações de
dimensão pela árvore durante a recursão. No final da passagem de medição, cada
View
armazena as medições. O framework realiza a segunda passagem em
layout(int, int, int, int)
e também de cima para baixo. Durante essa passagem, cada pai é responsável pelo posicionamento de todos os filhos
usando os tamanhos calculados na passagem de medição.
As duas passagens do processo de layout são descritas em mais detalhes nas seções a seguir.
Iniciar uma passagem de medição
Quando o método measure()
de um objeto View
retornar, defina os valores
getMeasuredWidth()
e
getMeasuredHeight()
com os valores de todos os dos descendentes do objeto View
. Os valores de largura e altura medidos de um objeto View
precisam respeitar as restrições impostas pelos
pais do objeto View
. Isso garante que, ao final da passagem de medição, todos os pais
aceitem todas as medidas dos filhos.
Um View
pai pode chamar measure()
mais de uma vez nos filhos. Por
exemplo, o pai pode medir os filhos uma vez com dimensões não especificadas para determinar os
tamanhos preferidos. Se a soma dos tamanhos não restritos dos filhos for muito grande ou muito pequena, o pai
poderá chamar measure()
novamente com valores que restringem os tamanhos dos filhos.
A passagem de medição usa duas classes para comunicar dimensões. A classe
ViewGroup.LayoutParams
é como objetos View
comunicam os tamanhos e posições de preferência. A classe base
ViewGroup.LayoutParams
descreve a largura e a altura preferenciais do
View
. Para cada dimensão, ele pode especificar uma das seguintes opções:
- Uma dimensão exata.
MATCH_PARENT
, o que significa que o tamanho preferido paraView
é o tamanho do pai, menos o padding.WRAP_CONTENT
, o que significa que o tamanho preferido daView
é grande o suficiente para delimitar o conteúdo, além do padding.
Existem subclasses de ViewGroup.LayoutParams
para diferentes subclasses de
ViewGroup
. Por exemplo,
RelativeLayout
tem a própria
subclasse de ViewGroup.LayoutParams
, que inclui a capacidade de centralizar objetos
View
filhos horizontal e verticalmente.
Os objetos MeasureSpec
são
usados para enviar os requisitos da árvore do pai para o filho. Um MeasureSpec
pode estar em
um dos três modos:
UNSPECIFIED
: o pai usa isso para determinar a dimensão de destino de umView
filho. Por exemplo, umLinearLayout
pode chamarmeasure()
no filho com a altura definida comoUNSPECIFIED
e uma largura deEXACTLY
240 para descobrir a altura que o filhoView
quer ter, com uma largura de 240 pixels.EXACTLY
: o pai usa isso para impor um tamanho exato ao filho. O filho precisa usar esse tamanho e garantir que todos os descendentes se encaixem nesse tamanho.AT MOST
: o pai usa isso para impor um tamanho máximo ao filho. O filho precisa garantir que ele e todos os descendentes se encaixem nesse tamanho.
Iniciar uma passagem de layout
Para iniciar um layout, chame
requestLayout()
. Normalmente,
esse método é chamado por uma View
quando acredita que não cabe mais
dentro dos limites.
Implementar uma lógica personalizada de medição e layout
Se você quiser implementar uma lógica personalizada de medição ou layout, substitua os métodos em que a lógica
está implementada:
onMeasure(int, int)
e
onLayout(boolean, int, int, int, int)
.
Esses métodos são chamados por measure(int, int)
e
layout(int, int, int, int)
, respectivamente. Não tente substituir os métodos
measure(int, int)
ou layout(int, int)
. Ambos os métodos
são tratados como "final
", por isso não podem ser substituídos.
O exemplo a seguir mostra como fazer isso na classe
`SplitLayout`
do app de exemplo
WindowManager.
Se o SplitLayout
tiver duas ou mais visualizações filhas e a tela tiver uma dobra,
ele posiciona as duas visualizações filhas em ambos os lados da dobra. O exemplo a seguir mostra um caso
de uso para substituir a medição e o layout. No entanto, para produção, use
SlidingPaneLayout
se quiser esse comportamento.
Kotlin
/** * An example of split-layout for two views, separated by a display * feature that goes across the window. When both start and end views are * added, it checks whether there are display features that separate the area * in two—such as a fold or hinge—and places them side-by-side or * top-bottom. */ class SplitLayout : FrameLayout { private var windowLayoutInfo: WindowLayoutInfo? = null private var startViewId = 0 private var endViewId = 0 private var lastWidthMeasureSpec: Int = 0 private var lastHeightMeasureSpec: Int = 0 ... fun updateWindowLayout(windowLayoutInfo: WindowLayoutInfo) { this.windowLayoutInfo = windowLayoutInfo requestLayout() } override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) { val startView = findStartView() val endView = findEndView() val splitPositions = splitViewPositions(startView, endView) if (startView != null && endView != null && splitPositions != null) { val startPosition = splitPositions[0] val startWidthSpec = MeasureSpec.makeMeasureSpec(startPosition.width(), EXACTLY) val startHeightSpec = MeasureSpec.makeMeasureSpec(startPosition.height(), EXACTLY) startView.measure(startWidthSpec, startHeightSpec) startView.layout( startPosition.left, startPosition.top, startPosition.right, startPosition.bottom ) val endPosition = splitPositions[1] val endWidthSpec = MeasureSpec.makeMeasureSpec(endPosition.width(), EXACTLY) val endHeightSpec = MeasureSpec.makeMeasureSpec(endPosition.height(), EXACTLY) endView.measure(endWidthSpec, endHeightSpec) endView.layout( endPosition.left, endPosition.top, endPosition.right, endPosition.bottom ) } else { super.onLayout(changed, left, top, right, bottom) } } /** * Gets the position of the split for this view. * @return A rect that defines of split, or {@code null} if there is no split. */ private fun splitViewPositions(startView: View?, endView: View?): Array? { if (windowLayoutInfo == null || startView == null || endView == null) { return null } // Calculate the area for view's content with padding. val paddedWidth = width - paddingLeft - paddingRight val paddedHeight = height - paddingTop - paddingBottom windowLayoutInfo?.displayFeatures ?.firstOrNull { feature -> isValidFoldFeature(feature) } ?.let { feature -> getFeaturePositionInViewRect(feature, this)?.let { if (feature.bounds.left == 0) { // Horizontal layout. val topRect = Rect( paddingLeft, paddingTop, paddingLeft + paddedWidth, it.top ) val bottomRect = Rect( paddingLeft, it.bottom, paddingLeft + paddedWidth, paddingTop + paddedHeight ) if (measureAndCheckMinSize(topRect, startView) && measureAndCheckMinSize(bottomRect, endView) ) { return arrayOf(topRect, bottomRect) } } else if (feature.bounds.top == 0) { // Vertical layout. val leftRect = Rect( paddingLeft, paddingTop, it.left, paddingTop + paddedHeight ) val rightRect = Rect( it.right, paddingTop, paddingLeft + paddedWidth, paddingTop + paddedHeight ) if (measureAndCheckMinSize(leftRect, startView) && measureAndCheckMinSize(rightRect, endView) ) { return arrayOf(leftRect, rightRect) } } } } // You previously tried to fit the children and measure them. Since they // don't fit, measure again to update the stored values. measure(lastWidthMeasureSpec, lastHeightMeasureSpec) return null } override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { super.onMeasure(widthMeasureSpec, heightMeasureSpec) lastWidthMeasureSpec = widthMeasureSpec lastHeightMeasureSpec = heightMeasureSpec } /** * Measures a child view and sees if it fits in the provided rect. * This method calls [View.measure] on the child view, which updates its * stored values for measured width and height. If the view ends up with * different values, measure again. */ private fun measureAndCheckMinSize(rect: Rect, childView: View): Boolean { val widthSpec = MeasureSpec.makeMeasureSpec(rect.width(), AT_MOST) val heightSpec = MeasureSpec.makeMeasureSpec(rect.height(), AT_MOST) childView.measure(widthSpec, heightSpec) return childView.measuredWidthAndState and MEASURED_STATE_TOO_SMALL == 0 && childView.measuredHeightAndState and MEASURED_STATE_TOO_SMALL == 0 } private fun isValidFoldFeature(displayFeature: DisplayFeature) = (displayFeature as? FoldingFeature)?.let { feature -> getFeaturePositionInViewRect(feature, this) != null } ?: false }
Java
/** * An example of split-layout for two views, separated by a display feature * that goes across the window. When both start and end views are added, it checks * whether there are display features that separate the area in two—such as * fold or hinge—and places them side-by-side or top-bottom. */ public class SplitLayout extends FrameLayout { @Nullable private WindowLayoutInfo windowLayoutInfo = null; private int startViewId = 0; private int endViewId = 0; private int lastWidthMeasureSpec = 0; private int lastHeightMeasureSpec = 0; ... void updateWindowLayout(WindowLayoutInfo windowLayoutInfo) { this.windowLayoutInfo = windowLayoutInfo; requestLayout(); } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { @Nullable View startView = findStartView(); @Nullable View endView = findEndView(); @Nullable ListsplitPositions = splitViewPositions(startView, endView); if (startView != null && endView != null && splitPositions != null) { Rect startPosition = splitPositions.get(0); int startWidthSpec = MeasureSpec.makeMeasureSpec(startPosition.width(), EXACTLY); int startHeightSpec = MeasureSpec.makeMeasureSpec(startPosition.height(), EXACTLY); startView.measure(startWidthSpec, startHeightSpec); startView.layout( startPosition.left, startPosition.top, startPosition.right, startPosition.bottom ); Rect endPosition = splitPositions.get(1); int endWidthSpec = MeasureSpec.makeMeasureSpec(endPosition.width(), EXACTLY); int endHeightSpec = MeasureSpec.makeMeasureSpec(endPosition.height(), EXACTLY); startView.measure(endWidthSpec, endHeightSpec); startView.layout( endPosition.left, endPosition.top, endPosition.right, endPosition.bottom ); } else { super.onLayout(changed, left, top, right, bottom); } } /** * Gets the position of the split for this view. * @return A rect that defines of split, or {@code null} if there is no split. */ @Nullable private List splitViewPositions(@Nullable View startView, @Nullable View endView) { if (windowLayoutInfo == null || startView == null || endView == null) { return null; } int paddedWidth = getWidth() - getPaddingLeft() - getPaddingRight(); int paddedHeight = getHeight() - getPaddingTop() - getPaddingBottom(); List displayFeatures = windowLayoutInfo.getDisplayFeatures(); @Nullable DisplayFeature feature = displayFeatures .stream() .filter(item -> isValidFoldFeature(item) ) .findFirst() .orElse(null); if (feature != null) { Rect position = SampleToolsKt.getFeaturePositionInViewRect(feature, this, true); Rect featureBounds = feature.getBounds(); if (featureBounds.left == 0) { // Horizontal layout. Rect topRect = new Rect( getPaddingLeft(), getPaddingTop(), getPaddingLeft() + paddedWidth, position.top ); Rect bottomRect = new Rect( getPaddingLeft(), position.bottom, getPaddingLeft() + paddedWidth, getPaddingTop() + paddedHeight ); if (measureAndCheckMinSize(topRect, startView) && measureAndCheckMinSize(bottomRect, endView)) { ArrayList rects = new ArrayList (); rects.add(topRect); rects.add(bottomRect); return rects; } } else if (featureBounds.top == 0) { // Vertical layout. Rect leftRect = new Rect( getPaddingLeft(), getPaddingTop(), position.left, getPaddingTop() + paddedHeight ); Rect rightRect = new Rect( position.right, getPaddingTop(), getPaddingLeft() + paddedWidth, getPaddingTop() + paddedHeight ); if (measureAndCheckMinSize(leftRect, startView) && measureAndCheckMinSize(rightRect, endView)) { ArrayList rects = new ArrayList (); rects.add(leftRect); rects.add(rightRect); return rects; } } } // You previously tried to fit the children and measure them. Since // they don't fit, measure again to update the stored values. measure(lastWidthMeasureSpec, lastHeightMeasureSpec); return null; } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); lastWidthMeasureSpec = widthMeasureSpec; lastHeightMeasureSpec = heightMeasureSpec; } /** * Measures a child view and sees if it fits in the provided rect. * This method calls [View.measure] on the child view, which updates * its stored values for measured width and height. If the view ends up with * different values, measure again. */ private boolean measureAndCheckMinSize(Rect rect, View childView) { int widthSpec = MeasureSpec.makeMeasureSpec(rect.width(), AT_MOST); int heightSpec = MeasureSpec.makeMeasureSpec(rect.height(), AT_MOST); childView.measure(widthSpec, heightSpec); return (childView.getMeasuredWidthAndState() & MEASURED_STATE_TOO_SMALL) == 0 && (childView.getMeasuredHeightAndState() & MEASURED_STATE_TOO_SMALL) == 0; } private boolean isValidFoldFeature(DisplayFeature displayFeature) { if (displayFeature instanceof FoldingFeature) { return SampleToolsKt.getFeaturePositionInViewRect(displayFeature, this, true) != null; } else { return false; } } }