Criar seu primeiro Bloco no Wear OS

Blocos deslizando entre a hora, o clima e o timer.

Assista à animação acima (demonstração de Blocos). Observação: os GIFs são animados apenas uma vez. Se você perdeu a animação, recarregue a página.

Os Blocos do Wear OS oferecem fácil acesso às informações e ações de que os usuários precisam para realizar tarefas. Com um simples gesto de deslizar no mostrador do relógio, o usuário pode ver a previsão mais recente ou iniciar um timer.

Desenvolver Blocos é um pouco diferente de criar um app.

Um Bloco é executado como parte da IU do sistema em vez de ser executado no próprio contêiner de aplicativo. Isso significa que ele não tem acesso a alguns conceitos de programação do Android que você talvez conheça, como Atividades e layouts XML.

Em vez disso, usamos um Service para descrever o layout e o conteúdo do Bloco. A IU do sistema renderizará o Bloco quando necessário.

Neste codelab, você aprenderá a criar seu próprio Bloco de Wear OS do zero.

O que você aprenderá

  • Criar um Bloco
  • Testar um Bloco em um dispositivo
  • Projetar um layout de Blocos
  • Adicionar uma imagem
  • Adicionar interação (toque)

O que você criará

Você criará um Bloco personalizado que mostra o número de etapas de uma meta diária. Ela inclui um layout complexo que ajudará você a aprender sobre diferentes elementos e contêineres de layout.

Veja como ele ficará quando você terminar o codelab:

c6e1959693cded21.png

Pré-requisitos

Nesta etapa, você configurará seu ambiente e fará o download de um projeto inicial.

O que é preciso

  • Versão estável mais recente do Android Studio
  • Dispositivo ou emulador do Wear OS? (Novo usuário? Veja aqui como configurar esse recurso.)

Fazer o download do código

Se você tiver o git instalado, execute o comando abaixo para clonar o código deste repositório. Para verificar se o git está instalado, digite git --version no terminal ou na linha de comando e verifique se ele é executado corretamente.

git clone https://github.com/googlecodelabs/wear-tiles.git
cd wear-tiles

Caso você não tenha o git, clique no botão abaixo para fazer o download de todo o código para este codelab:

Fazer o download do ZIP

Você pode executar qualquer um dos módulos no Android Studio a qualquer momento mudando a configuração de execução na barra de ferramentas.

8a2e49d6d6d2609d.png

Abrir o projeto no Android Studio

  1. Na janela "Welcome to Android Studio", selecione 1f5145c42df4129a.png Open an Existing Project.
  2. Selecione a pasta [Download Location]
  3. Depois que o Android Studio importar o projeto, teste se você pode executar os módulos start e finished em um emulador ou dispositivo físico do Wear OS.
  4. O módulo start será parecido com a captura de tela abaixo. É nele que você fará todo o trabalho.

c72e8870facd8458.png

Conhecer o código inicial

  • build.gradle contém uma configuração básica de app. Ele inclui as dependências necessárias para criar um Bloco.
  • main > AndroidManifest.xml inclui as partes necessárias para marcar o item como um app do Wear OS. Analisaremos esse arquivo durante o codelab.
  • main > GoalsRepository.kt contém uma classe de repositório falsa que recupera de maneira assíncrona um número aleatório de etapas que o usuário definiu hoje.
  • main > GoalsTileService.kt contém um código boilerplate para criar um Bloco. Faremos a maior parte do nosso trabalho nesse arquivo.
  • debug > AndroidManifest.xml contém um elemento de atividade para a TilePreviewActivity, para que possamos visualizar nosso bloco.
  • debug > TilesPreviewActivity.kt contém a atividade que usaremos para visualizar o Bloco.

Para começar, abra GoalsTileService no módulo start. Como você pode ver, essa classe estende TileProviderService.

TileProviderService faz parte da biblioteca de Blocos que fornece os métodos que usaremos para criar nosso Bloco:

  • onTileRequest(): cria um Bloco quando o sistema solicita.
  • onResourcesRequest(): fornece as imagens necessárias para o Bloco retornado em onTileRequest().

Estamos usando corrotinas para trabalhar com a natureza assíncrona desses métodos. Para saber mais sobre corrotinas, leia a documentação de corrotinas do Android.

Vamos começar criando um Bloco "Hello, world" simples.

Antes de começarmos, observe que definimos várias constantes no início do arquivo que serão usadas ao longo do codelab. Você poderá revisá-las se pesquisar "TODO: Review Constants". Elas definem vários elementos, incluindo a versão do recurso para valores dp (preenchimento, tamanho etc.), valores sp (texto), identificadores e muito mais.

Vamos começar a programar.

Em GoalsTileService.kt, pesquise por "TODO: Build a Tile" e substitua toda

a implementação de onTileRequest() pelo código abaixo.

Etapa 1

// TODO: Build a Tile.
override fun onTileRequest(requestParams: TileRequest) = serviceScope.future {

    Tile.builder()
        // If there are any graphics/images defined in the Tile's layout, the system will
        // retrieve them using onResourcesRequest() and match them with this version number.
        .setResourcesVersion(RESOURCES_VERSION)

        // Creates a timeline to hold one or more tile entries for a specific time periods.
        .setTimeline(
            Timeline.builder().addTimelineEntry(
                TimelineEntry.builder().setLayout(
                    Layout.builder().setRoot(
                        Text.builder().setText("Hello, world!")
                    )
                )
            )
        ).build()
}

O método onTileRequest() espera que um Future seja retornado, e usaremosserviceScope.future { ... } para converter a corrotina em um ListenableFuture em Java.

Fora isso, você só precisa criar um Tile na corrotina, então vamos fazer isso.

Dentro de onTileRequest(), usamos um padrão de builder para criar um Bloco "Hello, world!" . Leia os comentários no bloco de código para ver o que está acontecendo.

Primeiro, definimos uma versão do recurso. A versão do recurso retornada no payload desse método precisa corresponder à versão do recurso retornada de onResourcesRequest(), independentemente de algum recurso ser realmente usado.

É uma maneira de combinar esses gráficos com a versão correta quando o sistema chama onResourcesRequest() para consegui-los.

No entanto, para esse Bloco "Hello, world" simples, não temos gráficos.

Em seguida, criamos uma Timeline.

Uma Timeline consiste em uma ou mais instâncias de TimelineEntry. Cada uma delas descreve um layout para um intervalo de tempo específico. É possível criar vários valores de TimelineEntry que ocorrerão no futuro, e o sistema os processará nesses momentos posteriores.

fbb666b722376749.png

Leia mais sobre como trabalhar com linhas do tempo no guia de linha do tempo.

Neste exemplo, declaramos apenas uma instância de TimelineEntry porque queremos apenas um Bloco para todos os momentos. Em seguida, definimos o layout usando setLayout.

A raiz pode ser criada com um ou mais layouts complexos, mas, para esta etapa, criaremos um elemento de layout Text simples que exibe "Hello, world!".

Pronto!

Agora que criamos um Bloco muito simples, vamos visualizá-lo em uma Activity.

Para visualizar esse Bloco em uma atividade, abra a TilePreviewActivity na pasta de origem de debug do app.

Pesquise por "TODO: Review creation of Tile for Preview" e adicione o seguinte código após isso.

Etapa 2

// TODO: Review creation of Tile for Preview.
tileManager = TileManager(
    context = this,
    component = ComponentName(this, GoalsTileService::class.java),
    parentView = rootLayout
)
tileManager.create()

A maioria dessas instruções é autoexplicativa.

Você cria um TileManager, que podemos usar para visualizar o Bloco e definir o contexto, o componente (a classe de serviço do Bloco em que estamos trabalhando) e a visualização pai em que inserimos o Bloco.

Depois disso, criamos o Bloco usando create().

Você também perceberá que fizemos uma limpeza em onDestroy() usando tileManager.close().

Agora, execute o app no seu emulador ou dispositivo Wear OS. Escolha o módulo start. Você verá a mensagem "Hello, world!" dentro da tela:

b9976e1073554422.png

Agora que o layout básico está funcionando, vamos expandi-lo e criar um Bloco mais complexo. A meta final será este layout:

c6e1959693cded21.png

Substituir layout raiz por uma caixa

Substitua todo o método onTileRequest, o que inclui o texto "Hello, world!", pelo código abaixo.

Etapa 3

// TODO: Build a Tile.
override fun onTileRequest(requestParams: TileRequest) = serviceScope.future {

    // Retrieves progress value to populate the Tile.
    val goalProgress = GoalsRepository.getGoalProgress()
    // Retrieves font styles for any text in the Tile.
    val fontStyles = FontStyles.withDeviceParameters(requestParams.deviceParameters)

    // Creates Tile.
    Tile.builder()
        // If there are any graphics/images defined in the Tile's layout, the system will
        // retrieve them using onResourcesRequest() and match them with this version number.
        .setResourcesVersion(RESOURCES_VERSION)
        // Creates a timeline to hold one or more tile entries for a specific time periods.
        .setTimeline(
            Timeline.builder().addTimelineEntry(
                TimelineEntry.builder().setLayout(
                    Layout.builder().setRoot(
                        // Creates the root [Box] [LayoutElement]
                        layout(goalProgress, fontStyles)
                    )
                )
            )
        ).build()
}

Leia os comentários no bloco de código para ver o que está acontecendo. Ainda não definimos layout(), portanto, não se preocupe com o erro.

Na primeira parte do código no método, começamos recuperando o progresso real da meta de um repositório (falso).

Também recuperamos os deviceParameters passados para onTileRequest(), que usaremos posteriormente para criar o estilo de fonte dos nossos rótulos de texto.

A próxima parte do código é criar o Bloco novamente como antes. Observe que quase todo o código é o mesmo.

Na verdade, mudamos apenas a linha em que definimos a raiz como layout(goalProgress, fontStyles).

Como mencionado anteriormente, você verá uma linha vermelha sob o nome do método porque ele não existe.

É um padrão comum dividir o layout em várias partes lógicas e defini-las nos próprios métodos para evitar códigos profundamente aninhados. Agora, vamos definir o método de layout.

Pesquise por "TODO: Create root Box layout and content" e adicione o código abaixo.

Etapa 4

    // TODO: Create root Box layout and content.
    // Creates a simple [Box] container that lays out its children one over the other. In our
    // case, an [Arc] that shows progress on top of a [Column] that includes the current steps
    // [Text], the total steps [Text], a [Spacer], and a running icon [Image].
    private fun layout(goalProgress: GoalProgress, fontStyles: FontStyles) =
        Box.builder()
            // Sets width and height to expand and take up entire Tile space.
            .setWidth(expand())
            .setHeight(expand())

            // Adds an [Arc] via local function.
            .addContent(progressArc(goalProgress.percentage))

            // TODO: Add Column containing the rest of the data.
            // TODO: START REPLACE THIS LATER
            .addContent(
                Text.builder()
                    .setText("REPLACE ME!")
                    .setFontStyle(fontStyles.display3())
            )
            // TODO: END REPLACE THIS LATER

            .build()

Leia os comentários no bloco de código para ver o que está acontecendo. Ainda não definimos progressArc(). Portanto, não se preocupe com os erros nessa chamada de método ou em setRoot().

Nosso método de layout tem um contêiner Box simples, que exibe os filhos um sobre o outro. Dentro da caixa, adicionamos alguns tipos de conteúdo. Nesse caso, a chamada a um método que ainda não existe.

Seguiremos novamente o padrão de dividir métodos de layouts complexos, para que sejam mais fáceis de ler.

Você pode ver mais algumas TODOs perto da parte inferior do código (acima da chamada build()). Vamos voltar a elas mais tarde no codelab.

Por enquanto, vamos corrigir esse erro e definir o método local que finalmente declarará o arco que estávamos procurando.

Criar um ArcLine

Primeiro, criaremos um arco em volta da borda da tela, que é preenchido quando o usuário aumenta a contagem de etapas.

A API Tiles oferece várias opções de contêiner Arc. Usaremos ArcLine, que renderiza uma linha curva em torno do Arc.

Agora vamos definir essa função, para podermos eliminar esse erro desagradável.

Localize "TODO: Create a function that constructs an Arc representation of the current step progress" e adicione o código abaixo.

Etapa 5

    // TODO: Create a function that constructs an Arc representation of the current step progress.
    // Creates an [Arc] representing current progress towards steps goal.
    private fun progressArc(percentage: Float) = Arc.builder()
        .addContent(
            ArcLine.builder()
                // Uses degrees() helper to build an [AngularDimension] which represents progress.
                .setLength(degrees(percentage * ARC_TOTAL_DEGREES))
                .setColor(argb(ContextCompat.getColor(this, R.color.primary)))
                .setThickness(PROGRESS_BAR_THICKNESS)
        )
        // Element will start at 12 o'clock or 0 degree position in the circle.
        .setAnchorAngle(degrees(0.0f))
        // Aligns the contents of this container relative to anchor angle above.
        // ARC_ANCHOR_START - Anchors at the start of the elements. This will cause elements
        // added to an arc to begin at the given anchor_angle, and sweep around to the right.
        .setAnchorType(ARC_ANCHOR_START)
        .build()

Leia os comentários no bloco de código para ver o que está acontecendo. Observação: podem ocorrer erros em degrees(), argb() e ContextCompat. Se esse for o caso, basta importar as versões dos Blocos clicando neles.

Neste código, estamos retornando um Arc.Builder com uma ArcLine que representa o progresso da meta de etapas.

Definimos o comprimento como a porcentagem da nossa meta concluída (convertida em graus), a cor e a espessura.

Depois, especificamos onde o arco começa, com o tipo de âncora. Há várias opções diferentes para a âncora. Fique à vontade para testar todas elas depois do codelab.

Ok, concluímos esta parte.

Vamos ver como ela funciona. Execute o app novamente para ver algo parecido com isto:

ad79daf115d3b6a4.png

Adicionar um contêiner de coluna

Agora que nosso Bloco tem um bom indicador de progresso, vamos adicionar texto adequado.

Como adicionaremos vários campos de texto (e depois uma imagem), queremos que os itens estejam em uma Column no centro da tela.

Uma Column é outro dos muitos contêineres com layout de Bloco e nos permite distribuir elementos filhos verticalmente, um após o outro.

Encontre "TODO: Add Column containing the rest of the data" e substitua o conteúdo do texto temporário pelo código abaixo. Isso inclui tudo de TODO: START REPLACE THIS LATER a TODO: END REPLACE THIS LATER.

Lembre-se de não remover a chamada build() ao final do bloco de código do original.

Etapa 6

            // TODO: Add Column containing the rest of the data.
            // Adds a [Column] containing the two [Text] objects, a [Spacer], and a [Image].
            .addContent(
                Column.builder()
                    // Adds a [Text] using local function.
                    .addContent(
                        currentStepsText(goalProgress.current.toString(), fontStyles)
                    )
                    // Adds a [Text] using local function.
                    .addContent(
                        totalStepsText(
                            resources.getString(R.string.goal, goalProgress.goal),
                            fontStyles
                        )
                    )
                    // TODO: Add Spacer and Image representations of our step graphic.
                    // DO LATER
            )

Leia os comentários no bloco de código para ver o que está acontecendo.

Estamos criando uma coluna que adiciona dois tipos de conteúdo. Agora temos dois erros.

Alguma ideia do que está acontecendo?

Novamente, seguimos o padrão de dividir métodos dos layouts complexos. Podemos ignorar a DO LATER TODO.

Vamos corrigir esses erros.

Adicionar elementos de texto

Encontre "TODO: Create functions that construct/stylize Text representations of the step count & goal" e adicione o código a seguir abaixo.

Etapa 7

    // TODO: Create functions that construct/stylize Text representations of the step count & goal.
    // Creates a [Text] with current step count and stylizes it.
    private fun currentStepsText(current: String, fontStyles: FontStyles) = Text.builder()
        .setText(current)
        .setFontStyle(fontStyles.display2())
        .build()

    // Creates a [Text] with total step count goal and stylizes it.
    private fun totalStepsText(goal: String, fontStyles: FontStyles) = Text.builder()
        .setText(goal)
        .setFontStyle(fontStyles.title3())
        .build()

Leia os comentários no bloco de código para ver o que está acontecendo.

Isto é bem objetivo. Por meio de duas funções diferentes, criamos elementos de layout Text separados.

Como você pode ter imaginado, um elemento de layout Text renderiza uma string de texto e pode envolvê-la.

Determinamos o tamanho da fonte pelas constantes definidas anteriormente nessa classe e, por fim, definimos os estilos de fonte pelos estilos recuperados em onTileRequest().

Agora temos texto. Vejamos como ficou o visual do Bloco. Execute-o e veja algo parecido com isto.

9eaca483c7e51f38.png

Adicionar o nome da imagem a onTileRequest()

Adicionaremos uma imagem à parte final da nossa IU.

Encontre "TODO: Add Spacer and Image representations of our step graphic" e substitua DO LATER pelo código abaixo. Não apague o caractere ")" à direita.

Lembre-se de não remover a chamada build() ao final do bloco de código do original.

Etapa 8

                    // TODO: Add Spacer and Image representations of our step graphic.
                    // Adds a [Spacer].
                    .addContent(Spacer.builder().setHeight(VERTICAL_SPACING_HEIGHT))
                    // Adds an [Image] using local function.
                    .addContent(startRunButton())

Leia os comentários no bloco de código para ver o que está acontecendo.

Primeiro, adicionamos um elemento de layout Spacer para oferecer preenchimento entre os elementos, neste caso, a imagem a seguir.

Em seguida, adicionamos o conteúdo de um elemento de layout Image que renderiza uma imagem. Porém, como você percebeu pelo erro, estamos definindo isso em uma função local separada.

Localize "TODO: Create a function that constructs/stylizes a clickable Image of a running icon" e adicione o código abaixo.

Etapa 9

    // TODO: Create a function that constructs/stylizes a clickable Image of a running icon.
    // Creates a running icon [Image] that's also a button to refresh the tile.
    private fun startRunButton() =
        Image.builder()
            .setWidth(BUTTON_SIZE)
            .setHeight(BUTTON_SIZE)
            .setResourceId(ID_IMAGE_START_RUN)
            .setModifiers(
                Modifiers.builder()
                    .setPadding(
                        Padding.builder()
                            .setStart(BUTTON_PADDING)
                            .setEnd(BUTTON_PADDING)
                            .setTop(BUTTON_PADDING)
                            .setBottom(BUTTON_PADDING)
                    )
                    .setBackground(
                        Background.builder()
                            .setCorner(Corner.builder().setRadius(BUTTON_RADIUS))
                            .setColor(argb(ContextCompat.getColor(this, R.color.primaryDark)))
                    )
                    // TODO: Add click (START)
                    // DO LATER
                    // TODO: Add click (END)
            )
            .build()

Leia todo o bloco de código para ver o que está acontecendo.

Como sempre, ignore a seção DO LATER TODO por enquanto.

A maior parte do código é autoexplicativa. Definimos várias dimensões e estilos com constantes definidas na parte superior da classe. Para saber mais sobre modificadores, clique aqui.

O mais importante é a chamada de .setResourceId(ID_IMAGE_START_RUN).

Estamos definindo o nome da imagem como uma constante que definimos na parte superior da classe.

Como última etapa, precisamos mapear esse nome de constante para uma imagem real em nosso aplicativo.

Adicionar o mapeamento de imagem a onResourcesRequest()

Os Blocos não têm acesso a nenhum dos recursos do seu app. Isso significa que não é possível passar um ID de imagem do Android para um elemento de layout de imagem e esperar que ele seja resolvido. Em vez disso, você precisa modificar o método onResourcesRequest() e fornecer os recursos manualmente.

Há duas maneiras de fornecer imagens no método onResourcesRequest():

Use setAndroidResourceByResId() a fim de mapear o nome usado para a imagem anteriormente para uma imagem real.

Encontre "TODO: Supply resources (graphics) for the Tile" e substitua todo o método existente pelo código abaixo.

Etapa 10

    // TODO: Supply resources (graphics) for the Tile.
    override fun onResourcesRequest(requestParams: ResourcesRequest) = serviceScope.future {
        Resources.builder()
            .setVersion(RESOURCES_VERSION)
            .addIdToImageMapping(
                ID_IMAGE_START_RUN,
                ImageResource.builder()
                    .setAndroidResourceByResid(
                        AndroidImageResourceByResId.builder()
                            .setResourceId(R.drawable.ic_run)
                    )
            )
            .build()
    }

Leia todo o bloco de código para ver o que está acontecendo.

Como você viu nas primeiras etapas com onTileRequest(), definimos um número de versão do recurso.

Aqui, definimos esse mesmo número de versão em nosso builder de recursos para fazer a correspondência do Bloco com os recursos corretos.

Em seguida, precisamos mapear todos os elementos de layout Image que criamos para a imagem real usando o método addIdToImageMapping(). Veja que usamos o mesmo nome de constante anterior, ID_IMAGE_START_RUN, e agora definimos o drawable específico que queremos retornar em .setResourceId(R.drawable.ic_run. )

Agora, execute-o e você verá a IU concluída.

c6e1959693cded21.png

Como última etapa, adicionaremos uma ação de clique ao bloco. Isso pode abrir uma Activity no seu app para Wear OS ou, no caso deste codelab, acionar uma atualização para o próprio Bloco.

Encontre "TODO: Add click (START)" e substitua o comentário DO LATER pelo código abaixo.

Lembre-se de não remover a chamada build() ao final do bloco de código do original.

Etapa 11

                    // TODO: Add click (START)
                    .setClickable(
                        Clickable.builder()
                            .setId(ID_CLICK_START_RUN)
                            .setOnClick(ActionBuilders.LoadAction.builder())
                    )
                    // TODO: Add click (END)

Leia todo o bloco de código para ver o que está acontecendo.

Ao adicionar o modificador Clickable a um elemento de layout, é possível reagir a um usuário que toca nesse elemento. Como reação a um evento de clique, você pode executar duas ações:

Em nosso caso, definimos um modificador clicável, mas usamos LoadAction para atualizar o Bloco com a linha simples ActionBuilders.LoadAction.builder(). Isso aciona uma chamada para onTileRequest(), mas transmite o ID que definimos, ID_CLICK_START_RUN.

Se quiséssemos, poderíamos verificar o último ID clicável passado para onTileRequest() e renderizar um Bloco diferente com base nesse ID. Seria algo parecido com isto:

Etapa 12

// Example of getting the clickable Id
override fun onTileRequest(requestParams: TileRequest) = serviceScope.future {

    if (requestParams.state.lastClickableId == ID_CLICK_START_RUN) {
        // Create start run tile...
    } else {
        // Create default tile...
    }
}

Em nosso caso, não faremos isso. Apenas atualizamos o Bloco, e um novo valor é extraído do banco de dados simulado.

Para conhecer mais opções de interação com um Bloco, consulte nosso guia.

Agora execute o Bloco novamente. Ao clicar no botão, você verá que o valor de etapas muda.

e15bba88abc0d832.png

Você provavelmente imaginou que definimos o serviço que estamos editando em algum lugar do manifesto.

Localize "TODO: Review service" e você verá o código abaixo.

Não há etapa aqui, apenas uma revisão do código existente.

Etapa 12

<!-- TODO: Review service -->
<service
   android:name="com.example.wear.tiles.GoalsTileService"
   android:label="@string/fitness_tile_label"
   android:description="@string/tile_description"
   android:icon="@drawable/ic_run"
   android:permission="com.google.android.wearable.permission.BIND_TILE_PROVIDER">
   <intent-filter>
       <action android:name="androidx.wear.tiles.action.BIND_TILE_PROVIDER" />
   </intent-filter>

   <!-- The tile preview shown when configuring tiles on your phone -->
   <meta-data
       android:name="androidx.wear.tiles.PREVIEW"
       android:resource="@drawable/tile_goals" />
</service>

Leia todo o bloco de código para ver o que está acontecendo.

Isso é parecido com um serviço normal, mas adicionamos alguns elementos específicos ao Bloco:

  1. Permissão para vincular o provedor de Blocos
  2. Um filtro de intent que registra o serviço como um provedor de Blocos
  3. Metadados adicionais que especificam uma imagem prévia a ser visualizada no smartphone

Parabéns! Você aprendeu a criar um Bloco para Wear OS.

Qual é a próxima etapa?

Confira os outros codelabs do Wear OS:

Leia mais