Gerenciamento de Componentes Visuais Android: Views, ViewGroups e ViewStubs

Controles Visuais Arrastáveis e Comportamento de Toque

A manipulação interativa de elementos visuais é fundamental em aplicações Android. Um cenário comum é permitir que um usuário arraste um controle pela tela. O código a seguir ilustra a implementação de um ImageView arrastável que, ao ser solto, retorna à sua posição inicial. Este exemplo explora o ciclo de eventos de toque e as coordenadas de layout.

package com.example.appinterativo;

import android.content.Context;
import android.graphics.Rect;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.widget.ImageView;
import androidx.appcompat.app.AppCompatActivity;

public class MainActivity extends AppCompatActivity implements View.OnTouchListener {

    private ImageView draggableImage;
    private int initialX, initialY; // Posição inicial do toque
    private int currentX, currentY; // Posição atual do toque
    private Rect originalBounds = new Rect(); // Limites originais do View

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        draggableImage = findViewById(R.id.draggable_icon);
        draggableImage.setOnTouchListener(this);

        // Captura as dimensões do View após o layout inicial
        draggableImage.post(() -> {
            originalBounds.set(draggableImage.getLeft(), draggableImage.getTop(),
                               draggableImage.getRight(), draggableImage.getBottom());
        });
    }

    @Override
    public boolean onTouch(View view, MotionEvent event) {
        // As coordenadas getRawX/Y fornecem a posição absoluta na tela
        int rawX = (int) event.getRawX();
        int rawY = (int) event.getRawY();

        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                // Salva a posição do toque e a posição inicial do View
                initialX = rawX;
                initialY = rawY;
                currentX = initialX;
                currentY = initialY;
                break;

            case MotionEvent.ACTION_MOVE:
                // Calcula o deslocamento
                int deltaX = rawX - currentX;
                int deltaY = rawY - currentY;

                // Atualiza as coordenadas do View
                int newLeft = view.getLeft() + deltaX;
                int newTop = view.getTop() + deltaY;
                int newRight = view.getRight() + deltaX;
                int newBottom = view.getBottom() + deltaY;

                // Define limites da tela (exemplo: 0 a 720 para largura, 0 a 600 para altura)
                int parentWidth = 720; // Substituir por largura real do pai
                int parentHeight = 600; // Substituir por altura real do pai

                if (newLeft < 0) {
                    newLeft = 0;
                    newRight = newLeft + view.getWidth();
                } else if (newRight > parentWidth) {
                    newRight = parentWidth;
                    newLeft = newRight - view.getWidth();
                }

                if (newTop < 0) {
                    newTop = 0;
                    newBottom = newTop + view.getHeight();
                } else if (newBottom > parentHeight) {
                    newBottom = parentHeight;
                    newTop = newBottom - view.getHeight();
                }

                view.layout(newLeft, newTop, newRight, newBottom);

                // Atualiza a posição atual do toque
                currentX = rawX;
                currentY = rawY;
                break;

            case MotionEvent.ACTION_UP:
                // Retorna o View para a posição inicial
                view.layout(originalBounds.left, originalBounds.top,
                            originalBounds.right, originalBounds.bottom);
                break;
        }
        return true; // Indica que o evento foi consumido
    }
}

Notas importantes:

  • view.getLeft(), getTop(), getRight(), getBottom() fornecem as coordenadas do View em relação ao seu pai.
  • Em onCreate(), essas coordenadas podem ser zero antes do layout ser concluído. O uso de view.post() garente que as dimensões sejam obtidas após o processo de layout.
  • event.getRawX() e event.getRawY() fornecem as coordenadas absolutas do ponteiro na tela.
  • O método view.layout(left, top, right, bottom) define a posição e o tamanho do View.

Gerenciamento de Foco em Views

O foco é crucial para a interação do usuário, especialmente em cenários de navegação por teclado ou gamepad, ou para efeitos visuais como o "marquee" (texto em rolagem). Para habilitar o efeito marquee em um TextView, ele deve ser capaz de receber foco.

Configuração XML:

<TextView
    android:id="@+id/custom_text_view"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:ellipsize="marquee"
    android:focusable="true"
    android:focusableInTouchMode="true"
    android:marqueeRepeatLimit="marquee_forever"
    android:singleLine="true"
    android:text="Este é um texto longo para demonstrar o efeito de rolagem."
    android:textColor="#FF0000"
    android:textSize="18sp" />

  • android:focusable="true": Permite que o View receba foco.
  • android:focusableInTouchMode="true": Permite que o View receba foco mesmo quando em modo de toque (normalmente, o foco não é concedido em modo de toque, exceto para campos de texto).
  • android:ellipsize="marquee": Habilita o efeito marquee quando o texto é muito longo para a largura do View.
  • android:marqueeRepeatLimit="marquee_forever": Faz com que o marquee se repita indefinidamente.

Configuração programática:

Para definir o foco programaticamente, você pode usar os seguintes métodos:

TextView myTextView = findViewById(R.id.custom_text_view);
myTextView.setFocusable(true); // Permite que o View receba foco
myTextView.setFocusableInTouchMode(true); // Habilita foco em modo de toque
myTextView.requestFocus(); // Solicita o foco para este View

setFocusable(true) apenas indica que o View pode receber foco. requestFocus() é o método que de fato tenta dar o foco ao View.

Personalizando ViewGroups: Propriedades Essenciais

Ao criar um ViewGroup personalizado, algumas propriedades são importantes para controlar seu comportamento de desenho e foco em relação aos seus filhos.

public class CustomContainer extends LinearLayout {

    public CustomContainer(Context context) {
        super(context);
        initialize();
    }

    public CustomContainer(Context context, AttributeSet attrs) {
        super(context, attrs);
        initialize();
    }

    private void initialize() {
        // Permite que este ViewGroup desenhe seu próprio conteúdo.
        // Por padrão, ViewGroups não chamam onDraw se não tiverem background.
        setWillNotDraw(false); 

        // Define como o foco é distribuído entre os descendentes deste ViewGroup.
        // FOCUS_AFTER_DESCENDANTS significa que o ViewGroup ganha foco após seus filhos.
        setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        // Lógica de desenho personalizada para o ViewGroup
    }
}

  • setWillNotDraw(false): Por padrão, ViewGroups são otimizados para não desenhar seu próprio conteúdo (assumindo que apenas seus filhos desenham). Chamar setWillNotDraw(false) permite que o método onDraw() do ViewGroup seja invocado, o que é necessário se você deseja desenhar algo diretamente neste contêiner.
  • setDescendantFocusability(int focusability): Controla como um ViewGroup gerencia o foco em relação aos seus elementos filhos.
    • FOCUS_BEFORE_DESCENDANTS: O ViewGroup tentará obter foco antes de qualquer um de seus descendentes.
    • FOCUS_AFTER_DESCENDANTS: O ViewGroup tentará obter foco somente se nenhum de seus descendentes estiver focado.
    • FOCUS_BLOCK_DESCENDANTS: O ViewGroup bloqueará que seus descendentes obtenham foco.

requestLayout() vs. invalidate(): Atualização da Interface

Entender a diferença entre requestLayout() e invalidate() é crucial para otimizar o desempenho da interface do usuário em Android.

  • requestLayout(): Este método é chamado quando um View considera que não se encaixa mais em suas dimensões ou posição atuais. Ele solicita que seu pai refaça o ciclo de medição (onMeasure) e layout (onLayout) para recalcular a posição e o tamanho do View e, potencialmente, de seus irmãos. É apropriado quando as propriedades de layout (como margens, padding, tamanho) de um View mudam.
  • invalidate(): Este método é chamado quando um View precisa ser redesenhado. Ele força o sistema a redesenhar o View na próxima vez que a árvore de views for renderizada. É usado quando as propriedades visuais do View (cores, texto, imagem), mas não suas dimensões ou posição, mudam.

Em resumo, requestLayout() afeta o tamanho e a posição, enquanto invalidate() afeta apenas a aparência visual sem alterar o layout.

Capturando Miniaturas de Views

Gerar uma miniatura (thumbnail) de um View pode ser útil para diversas finalidades, como pré-visualizações de layouts, histórico de navegação ou operações de arrastar e soltar. O Android fornece mecanismos para renderizar um View em um Bitmap.

Aqui estão algumas abordagens para obter um Bitmap a partir de um View:

import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.view.View;
import android.util.Log;

public class ViewSnapshot {

    /**
     * Gera um Bitmap a partir de um View, usando o cache de desenho.
     * Esta é uma forma simples e eficiente para Views que já foram desenhados.
     * @param targetView O View a ser capturado.
     * @return Um Bitmap do View, ou null em caso de falha.
     */
    public static Bitmap createBitmapFromViewCache(View targetView) {
        if (targetView == null) {
            return null;
        }

        targetView.setDrawingCacheEnabled(true);
        targetView.buildDrawingCache();
        Bitmap cachedBitmap = targetView.getDrawingCache();

        if (cachedBitmap == null) {
            Log.e("ViewSnapshot", "Failed to get drawing cache for view: " + targetView);
            return null;
        }

        // Cria uma cópia imutável do Bitmap do cache
        Bitmap resultBitmap = Bitmap.createBitmap(cachedBitmap);

        // Limpa o cache para liberar memória
        targetView.destroyDrawingCache();
        targetView.setDrawingCacheEnabled(false);

        return resultBitmap;
    }

    /**
     * Desenha um View em um novo Bitmap, útil para Views que não estão visíveis
     * ou para ter controle total sobre o processo de desenho.
     * @param targetView O View a ser desenhado.
     * @param width A largura desejada do Bitmap.
     * @param height A altura desejada do Bitmap.
     * @return Um Bitmap do View, ou null em caso de falha.
     */
    public static Bitmap drawViewToBitmap(View targetView, int width, int height) {
        if (targetView == null || width <= 0 || height <= 0) {
            return null;
        }

        // Mede e dispõe o View para as dimensões desejadas
        int widthSpec = View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY);
        int heightSpec = View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY);
        targetView.measure(widthSpec, heightSpec);
        targetView.layout(0, 0, width, height);

        Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
        Canvas canvas = new Canvas(bitmap);

        // Desenha o View no Canvas
        targetView.draw(canvas);

        return bitmap;
    }
}

Observações:

  • O método createBitmapFromViewCache é geralmente mais simples, mas depende do cache de desenho estar atualizado. Lembre-se de sempre desabilitar e destruir o cache após o uso para evitar vazamentos de memória.
  • O método drawViewToBitmap oferece mais controle, permitindo que você especifique as dimensões do Bitmap, mesmo que o View ainda não tenha sido disposto na tela. É ideal para Views que não estão visíveis ou para gerar miniaturas de tamanhos específicos.

Otimização de Desenho em ViewGroups

Para ViewGroups que contêm muitos filhos, otimizar o processo de desenho pode melhorar significativamente o desempenho. Uma técnica é habilitar e gerenciar o cache de desenho dos filhos.

public class OptimizedViewGroup extends LinearLayout {
    // ... construtores ...

    /**
     * Habilita ou desabilita o cache de desenho para todos os filhos.
     * @param enabled Se true, habilita o cache; se false, desabilita.
     */
    public void setChildrenDrawingCacheStatus(boolean enabled) {
        final int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            final View childView = getChildAt(i);
            childView.setDrawingCacheEnabled(enabled);
            if (enabled) {
                // Força a construção do cache de desenho.
                // O parâmetro 'true' indica para usar a qualidade do cache de desenho do pai (se houver).
                childView.buildDrawingCache(true); 
            } else {
                childView.destroyDrawingCache();
            }
        }
    }

    /**
     * Define a qualidade do cache de desenho para os filhos.
     * Uma qualidade mais baixa pode reduzir o consumo de memória.
     * @param quality A qualidade do cache (e.g., View.DRAWING_CACHE_QUALITY_LOW).
     */
    public void setChildrenDrawingCacheQuality(int quality) {
        final int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            final View childView = getChildAt(i);
            childView.setDrawingCacheQuality(quality);
        }
    }
}

Além disso, ao finalizar operações que exigem cache de desenho, é fundamental liberá-lo para evitar vazamentos de memória:

// Exemplo de como limpar o cache em um ViewGroup personalizado
public void clearAllChildrenDrawingCaches() {
    final int count = getChildCount();
    for (int i = 0; i < count; i++) {
        final View child = getChildAt(i);
        child.setDrawingCacheEnabled(false);
        child.destroyDrawingCache();
    }
}

Captura de Tela em Android

A funcionalidade de captura de tela é essencial para depuração, feedback de usuário ou para criar efeitos visuais. A classe ScreenCaptureUtility demonstra como capturar a tela de uma Activity ou de um View específico, incluindo ScrollView e ListView.

import android.app.Activity;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.os.Environment;
import android.util.Log;
import android.view.View;
import android.widget.ListView;
import android.widget.ScrollView;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;

public class ScreenCaptureUtility {

    private static final String TAG = "ScreenCaptureUtility";

    /**
     * Captura uma imagem da tela de uma Activity completa.
     * @param activity A Activity da qual a captura de tela será feita.
     * @return Um Bitmap da tela da Activity.
     */
    public static Bitmap captureActivityScreen(Activity activity) {
        View decorView = activity.getWindow().getDecorView();
        decorView.setDrawingCacheEnabled(true);
        decorView.buildDrawingCache();
        Bitmap fullScreenBitmap = decorView.getDrawingCache();

        // Obtém a altura da barra de status
        Rect displayRect = new Rect();
        activity.getWindow().getDecorView().getWindowVisibleDisplayFrame(displayRect);
        int statusBarHeight = displayRect.top;

        // Obtém dimensões da tela
        int screenWidth = decorView.getWidth();
        int screenHeight = decorView.getHeight();

        // Cria o bitmap final, excluindo a barra de status
        Bitmap finalBitmap = Bitmap.createBitmap(fullScreenBitmap, 0, statusBarHeight,
                                                 screenWidth, screenHeight - statusBarHeight);

        decorView.destroyDrawingCache();
        decorView.setDrawingCacheEnabled(false);

        return finalBitmap;
    }

    /**
     * Captura um Bitmap de um View específico.
     * @param targetView O View a ser capturado.
     * @return Um Bitmap do View.
     */
    public static Bitmap captureViewBitmap(View targetView) {
        targetView.setDrawingCacheEnabled(true);
        targetView.buildDrawingCache();
        Bitmap bitmap = Bitmap.createBitmap(targetView.getDrawingCache());
        targetView.destroyDrawingCache();
        targetView.setDrawingCacheEnabled(false);
        return bitmap;
    }

    /**
     * Captura um Bitmap de um ScrollView completo, incluindo conteúdo que está fora da tela.
     * @param scrollView O ScrollView a ser capturado.
     * @return Um Bitmap do conteúdo completo do ScrollView.
     */
    public static Bitmap captureScrollViewBitmap(ScrollView scrollView) {
        int totalHeight = 0;
        for (int i = 0; i < scrollView.getChildCount(); i++) {
            totalHeight += scrollView.getChildAt(i).getHeight();
        }

        Bitmap bitmap = Bitmap.createBitmap(scrollView.getWidth(), totalHeight, Bitmap.Config.ARGB_8888);
        Canvas canvas = new Canvas(bitmap);
        scrollView.draw(canvas); // Desenha o ScrollView em seu Canvas

        return bitmap;
    }

    /**
     * Captura um Bitmap de um ListView completo.
     * Nota: Para ListViews muito longos, isso pode consumir muita memória.
     * @param listView O ListView a ser capturado.
     * @return Um Bitmap do conteúdo completo do ListView.
     */
    public static Bitmap captureListViewBitmap(ListView listView) {
        int totalHeight = 0;
        int maxChildHeight = 0;
        for (int i = 0; i < listView.getChildCount(); i++) {
            View child = listView.getChildAt(i);
            child.measure(View.MeasureSpec.makeMeasureSpec(listView.getWidth(), View.MeasureSpec.EXACTLY),
                          View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED));
            totalHeight += child.getMeasuredHeight();
            if (child.getMeasuredHeight() > maxChildHeight) {
                maxChildHeight = child.getMeasuredHeight();
            }
        }

        // Se o ListView estiver vazio ou não visível
        if (totalHeight == 0 && listView.getAdapter() != null && listView.getAdapter().getCount() > 0) {
            // Estimar altura se os filhos não puderem ser medidos diretamente (ex: altura dinâmica)
            totalHeight = maxChildHeight * listView.getAdapter().getCount();
            if (totalHeight == 0) totalHeight = listView.getHeight(); // Fallback
        } else if (totalHeight == 0) {
            totalHeight = listView.getHeight(); // Pelo menos captura o que está visível
        }


        Bitmap bitmap = Bitmap.createBitmap(listView.getWidth(), totalHeight, Bitmap.Config.ARGB_8888);
        Canvas canvas = new Canvas(bitmap);
        listView.draw(canvas);

        return bitmap;
    }

    /**
     * Salva um Bitmap em um arquivo PNG no armazenamento externo.
     * @param bitmap O Bitmap a ser salvo.
     * @param filename O nome do arquivo (ex: "screenshot.png").
     * @param directory O diretório (ex: Environment.getExternalStorageDirectory()).
     * @return true se salvo com sucesso, false caso contrário.
     */
    public static boolean saveBitmapToFile(Bitmap bitmap, String filename, File directory) {
        if (bitmap == null || directory == null || !directory.canWrite()) {
            Log.e(TAG, "Cannot save bitmap: invalid bitmap or directory.");
            return false;
        }

        File file = new File(directory, filename);
        FileOutputStream fos = null;
        try {
            fos = new FileOutputStream(file);
            bitmap.compress(Bitmap.CompressFormat.PNG, 90, fos); // 90% de qualidade para PNG
            fos.flush();
            Log.d(TAG, "Bitmap saved to: " + file.getAbsolutePath());
            return true;
        } catch (IOException e) {
            Log.e(TAG, "Error saving bitmap to file: " + file.getAbsolutePath(), e);
            return false;
        } finally {
            if (fos != null) {
                try {
                    fos.close();
                } catch (IOException e) {
                    Log.e(TAG, "Error closing FileOutputStream", e);
                }
            }
        }
    }

    // Exemplo de uso:
    public static void takeAndSaveActivityScreenshot(Activity activity, String filename) {
        Bitmap screenshot = captureActivityScreen(activity);
        if (screenshot != null) {
            saveBitmapToFile(screenshot, filename, Environment.getExternalStorageDirectory());
        }
    }
}

Permissões: Para salvar em armazenamento externo, adicione <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> ao seu AndroidManifest.xml e solicite-a em tempo de execução para Android 6.0 (API 23) ou superior.

Manipulação Programática de LayoutParams

As LayoutParams são um conjunto de propriedades que um View usa para informar ao seu ViewGroup pai como ele deseja ser medido e posicionado. Cada tipo de ViewGroup (e.g., RelativeLayout, LinearLayout, FrameLayout) tem sua própria subclasse de LayoutParams.

import android.widget.RelativeLayout;
import android.widget.TextView;
import android.util.DisplayMetrics;

// Em uma Activity ou Fragment
DisplayMetrics displayMetrics = getResources().getDisplayMetrics();
int screenWidth = displayMetrics.widthPixels;

TextView infoDisplay = findViewById(R.id.live_info_text); // Supondo que seja um TextView dentro de um RelativeLayout

// Obtém os parâmetros de layout atuais
RelativeLayout.LayoutParams layoutParams = 
    (RelativeLayout.LayoutParams) infoDisplay.getLayoutParams();

// Modifica as margens (exemplo: margem direita negativa para rolagem)
float desiredWidth = infoDisplay.getTextSize() * "Texto Exemplo Longo".length(); // Estimativa
if (screenWidth < desiredWidth) {
    layoutParams.rightMargin = (int) (screenWidth - desiredWidth);
} else {
    layoutParams.rightMargin = 0; // Garante que a margem não seja negativa se couber
}

// Aplica os novos parâmetros de layout ao View
infoDisplay.setLayoutParams(layoutParams);

É crucial obter o tipo correto de LayoutParams (e.g., RelativeLayout.LayoutParams) para o ViewGroup pai do seu View, caso contrário, ocorrerá um erro de conversão (ClassCastException).

Analisando ViewConfiguration e getScaledTouchSlop()

A classe ViewConfiguration fornece acesso a valores, tempos limite e distâncias padrão usados pelo framwork Android para interação com a interface do usuário. Um método notável é getScaledTouchSlop().

import android.content.Context;
import android.view.ViewConfiguration;

// Em uma classe que possui acesso ao Context
Context myContext = getApplicationContext(); // Ou this, se em Activity/Fragment
ViewConfiguration config = ViewConfiguration.get(myContext);

// Obtém a distância mínima que um toque deve percorrer antes de ser interpretado como um deslize.
int touchSlop = config.getScaledTouchSlop();

// 'touchSlop' agora contém o valor em pixels.
// Ele pode ser usado em implementações de OnTouchListener para distinguir entre
// um clique (pequeno movimento) e um deslize (movimento maior que touchSlop).

O getScaledTouchSlop() retorna a distância em pixels que o dedo do usuário deve percorrer na tela antes que o sistema interprete o movimento como um "arrasto" ou "deslize", em vez de um toque estacionário ou um clique. Isso ajuda a prevenir ações indesejadas causadas por pequenos movimentos involuntários do dedo durante um toque.

Construindo Layouts Programaticamente

Embora a maioria dos layouts Android seja definida em XML, é possível criar e manipular a hierarquia de Views inteiramente via código Java/Kotlin. Isso oferece flexibilidade, especialmente para layouts dinâmicos.

import android.content.Context;
import android.widget.Button;
import android.widget.RelativeLayout;
import android.view.ViewGroup;

public class DynamicLayoutBuilder {

    public static RelativeLayout createDirectionalLayout(Context context) {
        RelativeLayout layoutContainer = new RelativeLayout(context);
        layoutContainer.setLayoutParams(new ViewGroup.LayoutParams(
            ViewGroup.LayoutParams.MATCH_PARENT, 
            ViewGroup.LayoutParams.MATCH_PARENT));

        // Botões de exemplo
        Button btnCenter = new Button(context);
        btnCenter.setId(View.generateViewId()); // Gera um ID único
        btnCenter.setText("Centro");
        
        Button btnTop = new Button(context);
        btnTop.setId(View.generateViewId());
        btnTop.setText("Superior");

        Button btnBottom = new Button(context);
        btnBottom.setId(View.generateViewId());
        btnBottom.setText("Inferior");

        Button btnLeft = new Button(context);
        btnLeft.setId(View.generateViewId());
        btnLeft.setText("Esquerda");

        Button btnRight = new Button(context);
        btnRight.setId(View.generateViewId());
        btnRight.setText("Direita");

        // Parâmetros de layout comuns
        RelativeLayout.LayoutParams commonParams = new RelativeLayout.LayoutParams(
            ViewGroup.LayoutParams.WRAP_CONTENT, 
            ViewGroup.LayoutParams.WRAP_CONTENT);

        // Parâmetros para o botão central
        RelativeLayout.LayoutParams centerParams = new RelativeLayout.LayoutParams(commonParams);
        centerParams.addRule(RelativeLayout.CENTER_IN_PARENT, RelativeLayout.TRUE);
        layoutContainer.addView(btnCenter, centerParams);

        // Parâmetros para o botão superior
        RelativeLayout.LayoutParams topParams = new RelativeLayout.LayoutParams(commonParams);
        topParams.addRule(RelativeLayout.ABOVE, btnCenter.getId());
        topParams.addRule(RelativeLayout.ALIGN_LEFT, btnCenter.getId());
        layoutContainer.addView(btnTop, topParams);

        // Parâmetros para o botão inferior
        RelativeLayout.LayoutParams bottomParams = new RelativeLayout.LayoutParams(commonParams);
        bottomParams.addRule(RelativeLayout.BELOW, btnCenter.getId());
        bottomParams.addRule(RelativeLayout.ALIGN_LEFT, btnCenter.getId());
        layoutContainer.addView(btnBottom, bottomParams);

        // Parâmetros para o botão esquerdo
        RelativeLayout.LayoutParams leftParams = new RelativeLayout.LayoutParams(commonParams);
        leftParams.addRule(RelativeLayout.LEFT_OF, btnCenter.getId());
        leftParams.addRule(RelativeLayout.ALIGN_BASELINE, btnCenter.getId());
        layoutContainer.addView(btnLeft, leftParams);

        // Parâmetros para o botão direito
        RelativeLayout.LayoutParams rightParams = new RelativeLayout.LayoutParams(commonParams);
        rightParams.addRule(RelativeLayout.RIGHT_OF, btnCenter.getId());
        rightParams.addRule(RelativeLayout.ALIGN_BASELINE, btnCenter.getId());
        layoutContainer.addView(btnRight, rightParams);

        return layoutContainer;
    }
}

Este exemplo cria cinco botões dispostos ao redor de um centro dentro de um RelativeLayout, usando addRule() para definir suas relações posicionais.

Compreendendo addView() em ViewGroups

O método addView() é usado para adicionar um View filho a um ViewGroup. Ele possui várias sobrecargas, sendo uma das mais versáteis addView(View child, int index, ViewGroup.LayoutParams params).

  • child: O View a ser adicionado.
  • index: A posição na qual o View deve ser adicionado na lista de filhos do ViewGroup.
    • Um índice de 0 adiciona o View no início (ou "acima" de todos os outros filhos).
    • Um índice de -1 (ou usar a sorbecarga addView(View child)) adiciona o View no final (ou "abaixo" de todos os outros filhos).
    • Um índice maior que o número atual de filhos resultará em um erro IndexOutOfBoundsException.
  • params: Os LayoutParams que definem como o View deve ser medido e disposto pelo ViewGroup pai.

Por exemplo, para adicionar um cabeçalho (headView) ou um rodapé (footView) a um contêiner:

ViewGroup parentLayout = findViewById(R.id.my_parent_layout); // Supondo um LinearLayout
View headerView = getLayoutInflater().inflate(R.layout.header_layout, parentLayout, false);
View footerView = getLayoutInflater().inflate(R.layout.footer_layout, parentLayout, false);

// Adicionar cabeçalho na primeira posição (índice 0)
parentLayout.addView(headerView, 0); 

// Adicionar rodapé na última posição (índice -1, ou simplesmente addView(footerView))
parentLayout.addView(footerView, -1); 
// Equivalente a: parentLayout.addView(footerView);

Carregamento Dinâmico de Layouts com LayoutInflater

O LayoutInflater é uma classe essencial para inflar arquivos de layout XML em objetos View em tempo de execução. Quando se trabalha com layouts que precisam se adaptar a diferentes configurações (como orientação da tela), o uso de <merge> e a inflação com um ViewGroup pai podem otimizar a hierarquia de Views.

Considere dois arquivos de layout que podem ser usados como cabeçalho de uma lista, um para orientação horizontal e outro para vertical:

res/layout/pull_to_refresh_header_horizontal.xml

<merge xmlns:android="http://schemas.android.com/apk/res/android">
    <FrameLayout
        android:id="@+id/fl_inner"
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        android:padding="8dp">

        <ImageView
            android:id="@+id/pull_to_refresh_image"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center" />

        <ProgressBar
            android:id="@+id/pull_to_refresh_progress"
            style="?android:attr/progressBarStyleSmall"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:indeterminate="true"
            android:visibility="gone" />
    </FrameLayout>
</merge>

res/layout/pull_to_refresh_header_vertical.xml

<merge xmlns:android="http://schemas.android.com/apk/res/android">
    <FrameLayout
        android:id="@+id/fl_inner"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:padding="8dp">

        <FrameLayout
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="left|center_vertical">
            <ImageView
                android:id="@+id/pull_to_refresh_image"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_gravity="center" />
            <ProgressBar
                android:id="@+id/pull_to_refresh_progress"
                style="?android:attr/progressBarStyleSmall"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_gravity="center"
                android:indeterminate="true"
                android:visibility="gone" />
        </FrameLayout>

        <LinearLayout
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:gravity="center_horizontal"
            android:orientation="vertical">

            <TextView
                android:id="@+id/pull_to_refresh_text"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:singleLine="true"
                android:textAppearance="?android:attr/textAppearance"
                android:textStyle="bold" />

            <TextView
                android:id="@+id/pull_to_refresh_sub_text"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:singleLine="true"
                android:textAppearance="?android:attr/textAppearanceSmall"
                android:visibility="gone" />
        </LinearLayout>
    </FrameLayout>
</merge>

Em uma classe personalizada, você pode carregar o layout apropriado dinamicamente:

import android.content.Context;
import android.content.res.TypedArray;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.ProgressBar;
import android.widget.TextView;

public abstract class BaseLoadingLayout extends FrameLayout {

    protected FrameLayout innerContainer;
    protected TextView mainText;
    protected ProgressBar loadingProgress;
    protected TextView subText;
    protected ImageView statusImage;

    // Enum para a direção de rolagem, por exemplo
    public enum ScrollDirection { HORIZONTAL, VERTICAL }

    public BaseLoadingLayout(Context context, AttributeSet attrs, ScrollDirection direction) {
        super(context, attrs);
        LayoutInflater inflater = LayoutInflater.from(context);

        // Infla o layout apropriado e o anexa a 'this' (o próprio BaseLoadingLayout)
        if (direction == ScrollDirection.HORIZONTAL) {
            inflater.inflate(R.layout.pull_to_refresh_header_horizontal, this, true);
        } else {
            inflater.inflate(R.layout.pull_to_refresh_header_vertical, this, true);
        }

        // Como 'this' é o pai raiz do layout inflado (devido a attachToRoot=true no inflate),
        // findViewById pode encontrar elementos dentro do layout carregado.
        innerContainer = findViewById(R.id.fl_inner);
        mainText = innerContainer.findViewById(R.id.pull_to_refresh_text);
        loadingProgress = innerContainer.findViewById(R.id.pull_to_refresh_progress);
        subText = innerContainer.findViewById(R.id.pull_to_refresh_sub_text); // Pode ser null para horizontal
        statusImage = innerContainer.findViewById(R.id.pull_to_refresh_image);
    }
}

Ao usar LayoutInflater.inflate(R.layout.some_layout, this, true), o this (que é a instância de BaseLoadingLayout) se torna o ViewGroup raiz do layout inflado. Isso significa que qualquer chamada a findViewById() diretamente no BaseLoadingLayout (ou em seu filho innerContainer) será capaz de localizar os Views definidos nos arquivos XML carregados, pois eles agora fazem parte da hierarquia do BaseLoadingLayout.

Otimização de Layout com ViewStub

ViewStub é um View leve e invisível que pode ser usado para inflar layouts de forma preguiçosa (on-demand). Ele é uma ferramenta poderosa para otimização de desempenho, especialmente para partes da UI que raramente são visíveis ou dependem de condições específicas para serem exibidas.

Características do ViewStub:

  1. Leveza: No momento da inflação do layout principal, o ViewStub ocupa pouquíssima memória e tempo de processamento, pois não desenha nada.
  2. Ocupa lugar: Ele serve como um "placeholder" na hierarquia de Views.
  3. Inflação Condicional: O layout referenciado pelo ViewStub (através do atributo android:layout) só é inflado quando solicitado.
  4. Substituição: Uma vez inflado, o ViewStub é removido da hierarquia de Views e substituído pelo layout que ele inflou.
  5. Inflação Única: Um ViewStub só pode ser inflado uma vez. Tentativas subsequentes de inflar o mesmo ViewStub resultarão em um erro.

Cenários de Uso:

  • Telas de "estado vazio" (Empty State Views) para listas ou resultados de busca.
  • Barra de progresso ou mensagens de erro que aparecem apenas sob certas condições.
  • Layouts de administração ou configurações avançadas que poucos usuários acessam.

Exemplo de Uso:

No XML do layout principal:

<LinearLayout 
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <ListView
        android:id="@+id/data_list_view"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1" />

    <ViewStub
        android:id="@+id/empty_data_viewstub"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout="@layout/empty_list_layout" />

</LinearLayout>

E o layout para o estado vazio (res/layout/empty_list_layout.xml):

<LinearLayout 
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical"
    android:gravity="center"
    android:padding="16dp">

    <ImageView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@drawable/ic_empty_state" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Nenhum dado encontrado."
        android:textSize="18sp"
        android:textColor="@android:color/darker_gray"
        android:layout_marginTop="8dp" />

</LinearLayout>

No código Java/Kotlin para exibir/ocultar o estado vazio:

import android.os.Bundle;
import android.view.View;
import android.view.ViewStub;
import android.widget.ListView;
import androidx.appcompat.app.AppCompatActivity;

public class MyListActivity extends AppCompatActivity {

    private ListView listView;
    private ViewStub emptyStateViewStub;
    private View emptyStateView; // Referência ao layout inflado do ViewStub

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_my_list);

        listView = findViewById(R.id.data_list_view);
        emptyStateViewStub = findViewById(R.id.empty_data_viewstub);

        // Supondo que você tem um Adapter e dados para o ListView
        // List<String> data = new ArrayList<>();
        // ArrayAdapter<String> adapter = new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, data);
        // listView.setAdapter(adapter);

        // Exemplo: Simular ausência de dados
        boolean hasData = false; 

        if (hasData) {
            showListViewContent();
        } else {
            showEmptyState();
        }
    }

    private void showEmptyState() {
        listView.setVisibility(View.GONE);
        if (emptyStateView == null) {
            // Infla o ViewStub pela primeira vez
            emptyStateView = emptyStateViewStub.inflate();
        } else {
            // Se já foi inflado, apenas torna o View visível
            emptyStateView.setVisibility(View.VISIBLE);
        }
    }

    private void showListViewContent() {
        listView.setVisibility(View.VISIBLE);
        if (emptyStateView != null) {
            // Oculta o View do estado vazio se ele já foi inflado
            emptyStateView.setVisibility(View.GONE);
        }
        // Se emptyStateView ainda for null, significa que nunca foi inflado,
        // então não há necessidade de ocultá-lo.
    }
}

Posicionamento de Views Criados Dinamicamente

Para adicionar um View programaticamente e posicioná-lo com base nas propriedades de layout de um View existente, você pode reutilizar os LayoutParams. Isso é útil para criar elementos visuais que aparecem ou desaparecem em locais específicos da UI.

import android.content.Context;
import android.graphics.drawable.Drawable;
import android.widget.ImageView;
import android.widget.RelativeLayout;
import android.widget.FrameLayout.LayoutParams; // Ou o LayoutParams do ViewGroup pai

// Em um método de Activity ou similar
Context currentContext = this; // Ou o Context apropriado
RelativeLayout mainLayout = findViewById(R.id.main_relative_layout); // Layout pai
ImageView referenceImage = findViewById(R.id.existing_image_view); // View de referência
Drawable newIconDrawable = getResources().getDrawable(R.drawable.new_icon); // Drawable para o novo View

// 1. Obtém os LayoutParams do View de referência
ViewGroup.LayoutParams refParams = referenceImage.getLayoutParams();

// 2. Cria um novo ImageView programaticamente
ImageView dynamicIcon = new ImageView(currentContext);

// 3. Define os LayoutParams do novo ImageView para serem os mesmos do View de referência
dynamicIcon.setLayoutParams(refParams);

// 4. Configura propriedades visuais do novo ImageView
dynamicIcon.setScaleType(ImageView.ScaleType.FIT_CENTER);
dynamicIcon.setImageDrawable(newIconDrawable);

// 5. Adiciona o novo ImageView ao layout pai
mainLayout.addView(dynamicIcon);

// 6. Define a posição exata do novo ImageView com base na referência (opcional, se os LayoutParams não forem suficientes)
// Isso é útil se o View de referência já está totalmente layout e você quer replicar sua posição pixel a pixel.
dynamicIcon.layout(referenceImage.getLeft(), referenceImage.getTop(), 
                   referenceImage.getRight(), referenceImage.getBottom());

O método layout() no passo 6 é particularmente útil se você precisa garantir que o novo View ocupe exatamente a mesma área do View de referência, independentemente das regras do LayoutParams, uma vez que o layout pai já calculou as posições.

Modificando Dimensões de Views Programaticamente

Ajustar a largura e altura de um View em tempo de execução é uma tarefa comum e é feita manipulando seus LayoutParams.

import android.widget.Button;
import android.widget.RelativeLayout; // ou LinearLayout, FrameLayout, dependendo do pai

// Em um método de Activity ou Fragment
Button actionButton = findViewById(R.id.action_button); // O botão cujas dimensões serão alteradas

// 1. Obtém os LayoutParams atuais do View. 
// Certifique-se de fazer um cast para a subclasse correta de LayoutParams 
// que corresponde ao ViewGroup pai do View.
RelativeLayout.LayoutParams currentParams = 
    (RelativeLayout.LayoutParams) actionButton.getLayoutParams();

// 2. Modifica a largura e altura
// Use getResources().getDimensionPixelSize() para obter valores de dimensões definidos em XML (e.g., dimen.xml)
currentParams.height = getResources().getDimensionPixelSize(R.dimen.custom_height_medium); 
currentParams.width = getResources().getDimensionPixelSize(R.dimen.custom_width_large); 

// Você também pode definir valores em pixels diretamente
// currentParams.height = 100; // 100 pixels
// currentParams.width = 150; // 150 pixels

// 3. Aplica os LayoutParams modificados de volta ao View
actionButton.setLayoutParams(currentParams);

É importante notar que, após alterar os LayoutParams, o sistema de layout Android pode precisar de um ciclo de layout para aplicar as novas dimensões e posições. Em muitos casos, isso acontece automaticamente; em outros, pode ser necessário chamar requestLayout() no View ou em seu pai.

Obtendo Altura e Largura de Views com OnGlobalLayoutListener

Em métodos como onCreate() de uma Activity, as dimensões (largura e altura) de um View frequentemente retornam zero. Isso ocorre porque o ciclo de medição e layout dos Views ainda não foi concluído. Para obter as dimensões reais de um View após ele ter sido disposto na tela, pode-se usar o ViewTreeObserver, especificamente o OnGlobalLayoutListener.

O ViewTreeObserver é um observador que permite registrar callbacks para eventos específicos na árvore de Views, como mudanças de layout, foco ou modo de toque.

import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.view.ViewTreeObserver;
import androidx.appcompat.app.AppCompatActivity;

public class LayoutDimensionsActivity extends AppCompatActivity {

    private static final String TAG = "LayoutDimensionsActivity";
    private View headerView; // O View cuja altura queremos obter

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main_layout_dims);

        headerView = findViewById(R.id.my_header_view);

        // Usando um OnGlobalLayoutListener para capturar as dimensões após o layout
        headerView.getViewTreeObserver().addOnGlobalLayoutListener(
            new ViewTreeObserver.OnGlobalLayoutListener() {
                @Override
                public void onGlobalLayout() {
                    // Aqui, as dimensões do headerView já estão disponíveis
                    int headerHeight = headerView.getHeight();
                    int headerWidth = headerView.getWidth();
                    Log.d(TAG, "Altura do cabeçalho: " + headerHeight + ", Largura: " + headerWidth);

                    // É crucial remover o listener para evitar que ele seja chamado múltiplas vezes
                    // e para evitar vazamentos de memória.
                    // Compatibilidade para versões antigas:
                    if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) {
                        headerView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
                    } else {
                        // Método deprecado para APIs < 16
                        headerView.getViewTreeObserver().removeGlobalOnLayoutListener(this);
                    }
                }
            });
    }
}

Outros Listeners do ViewTreeObserver:

  • OnGlobalFocusChangeListener: Chamado quando o foco muda na árvore de Views.
  • OnPreDrawListener: Chamado imediatamente antes da árvore de Views ser desenhada. Retornar true cancela o desenho, false permite.
  • OnScrollChangedListener: Notifica quando qualquer View na árvore de Views muda sua posição de rolagem.
  • OnTouchModeChangeListener: Chamado quando o modo de toque muda na árvore de Views.

Por Que ViewGroups Não Chamam onDraw() por Padrão

Normalmente, se você criar um ViewGroup personalizado (como estender LinearLayout ou RelativeLayout) e sobrescrever seu método onDraw(Canvas canvas), perceberá que este método não é invocado. Este comportamento é uma otimização de desempenho do framework Android.

A Razão:

A maioria dos ViewGroups são contêineres que servem apenas para organizar seus filhos. Eles não têm conteúdo visual próprio para desenhar. Por padrão, ViewGroups são marcados com a flag WILL_NOT_DRAW, o que informa ao sistema que não há necessidade de chamar seu método onDraw(), economizando recursos.

Se um ViewGroup possui um plano de fundo definido (via android:background no XML ou setBackground() no código), o sistema assume que há algo para desenhar e, então, o onDraw() será invocado. O plano de fundo é desenhado antes dos filhos, e então o onDraw() do ViewGroup é chamado.

Como Habilitar onDraw() em um ViewGroup Personalizado:

Se você realmente precisa que seu ViewGroup personalizado desenhe seu próprio conteúdo (por exemplo, linhas de grade, indicadores), você deve explicitamente informar ao sistema que ele irá desenhar.

Existem duas maneiras de fazer isso:

  1. Definir um plano de fundo: Atribua qualquer cor ou drawable como plano de fundo do ViewGroup.
  2. Chamar setWillNotDraw(false): No construtor do seu ViewGroup personalizado, defina explicitamente a flag WILL_NOT_DRAW como falsa.
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.widget.LinearLayout;

public class CustomDrawingLayout extends LinearLayout {

    private Paint borderPaint;

    public CustomDrawingLayout(Context context) {
        super(context);
        init();
    }

    public CustomDrawingLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    private void init() {
        // Opção 1: Chamar setWillNotDraw(false) para garantir que onDraw seja chamado
        setWillNotDraw(false); 

        // Opcional: Definir um background também faria onDraw ser chamado,
        // mas setWillNotDraw(false) é mais direto se o objetivo é desenhar.
        // setBackgroundColor(Color.TRANSPARENT); // Um background transparente também funciona

        borderPaint = new Paint();
        borderPaint.setColor(Color.RED);
        borderPaint.setStrokeWidth(5);
        borderPaint.setStyle(Paint.Style.STROKE);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        // Lógica de desenho personalizada: desenha um contorno vermelho ao redor do ViewGroup
        canvas.drawRect(0, 0, getWidth(), getHeight(), borderPaint);
    }
}

Ao usar setWillNotDraw(false), você garante que onDraw() do seu ViewGroup será invocado, permitindo que você adicione lógica de desenho personalizada.

O Ciclo de Layout: onLayout do ViewGroup e layout do View

O processo de layout no Android é fundamental para posicionar e dimensionar cada View na tela. Ele envolve a interação entre os ViewGroups (que organizam seus filhos) e os Views (que se definem).

  • **onLayout(boolean changed, int left, int top, int right, int bottom) (definido em ViewGroup):**Este é um método abstrato em ViewGroup que deve ser implementado por qualquer subclasse de ViewGroup. Sua responsabilidade é posicionar cada um de seus Views filhos.

    Os parâmetros left, top, right, bottom representam o espaço retangular disponível para o ViewGroup pai (após considerar margens e padding). Dentro de onLayout, o ViewGroup iterará sobre seus filhos e chamará o método layout() de cada filho para definir suas posições e tamanhos relativos ao próprio ViewGroup.

  • **layout(int left, int top, int right, int bottom) (definido em View):**Este método é o que realmente define a posição e o tamanho de um View. Ele é invocado pelo ViewGroup pai durante seu método onLayout(). Os parâmetros left, top, right, bottom indicam as coordenadas do canto superior esquerdo e inferior direito do View, *relativas ao seu ViewGroup pai*.

    Quando um View recebe a chamada layout(), ele usa essas coordenadas para definir seus próprios limites, o que, por sua vez, afeta como ele é desenhado.

import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;

public class SimpleCustomLayout extends ViewGroup {

    public SimpleCustomLayout(Context context) {
        super(context);
    }

    public SimpleCustomLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        // Mede todos os filhos para determinar o tamanho total do ViewGroup
        measureChildren(widthMeasureSpec, heightMeasureSpec);

        int maxWidth = 0;
        int totalHeight = 0;

        // Exemplo simples: um ViewGroup que empilha Views verticalmente
        for (int i = 0; i < getChildCount(); i++) {
            View child = getChildAt(i);
            if (child.getVisibility() != GONE) {
                maxWidth = Math.max(maxWidth, child.getMeasuredWidth());
                totalHeight += child.getMeasuredHeight();
            }
        }

        // Define as dimensões do próprio ViewGroup
        setMeasuredDimension(resolveSize(maxWidth, widthMeasureSpec), 
                             resolveSize(totalHeight, heightMeasureSpec));
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        int currentY = 0; // Posição Y atual para empilhar filhos

        // Itera sobre os filhos e os posiciona
        for (int i = 0; i < getChildCount(); i++) {
            View child = getChildAt(i);
            if (child.getVisibility() != GONE) {
                int childWidth = child.getMeasuredWidth();
                int childHeight = child.getMeasuredHeight();

                // Posiciona o filho em 0 (X) e currentY (Y), com suas dimensões medidas
                child.layout(0, currentY, childWidth, currentY + childHeight);
                currentY += childHeight; // Atualiza Y para o próximo filho
            }
        }
    }
}

Neste exemplo, SimpleCustomLayout é um ViewGroup que simplesmente empilha seus filhos verticalmente. O método onMeasure calcula o tamanho necessário, e onLayout usa o método child.layout() para posicionar cada filho um abaixo do outro.

android,view,viewgroup,viewstub,layout,customview,touch-events,focus-management,bitmap,drawing-cache,performance,ui-development

Tags: android view viewgroup viewstub layout

Publicado em 6-8 16:47 por Thomas