Rolar cartão

Com o Google Glass, você pode criar interações avançadas com seus cartões, como rolagem e animações.

Rolar cards em atividades

A tela e o touchpad do Glass são ótimos para mostrar cartões deslizáveis, como na linha do tempo do Glass. Se você estiver criando uma atividade, poderá criar o mesmo tipo de efeito com o widget CardScrollView.

  1. Implemente um CardScrollAdapter para fornecer cards ao CardScrollView. Você pode criar uma hierarquia de visualização padrão ou usar a classe CardBuilder.
  2. Crie um CardScrollView que use o CardScrollAdapter como o fornecedor dos cartões.
  3. Defina a visualização do conteúdo da atividade como CardScrollView ou exiba a CardScrollView em um layout.

Veja uma implementação simples que rola por três cards:

public class CardScrollActivity extends Activity {

    private List<CardBuilder> mCards;
    private CardScrollView mCardScrollView;
    private ExampleCardScrollAdapter mAdapter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        createCards();

        mCardScrollView = new CardScrollView(this);
        mAdapter = new ExampleCardScrollAdapter();
        mCardScrollView.setAdapter(mAdapter);
        mCardScrollView.activate();
        setContentView(mCardScrollView);
    }

    private void createCards() {
        mCards = new ArrayList<CardBuilder>();

        mCards.add(new CardBuilder(this, CardBuilder.Layout.TEXT)
                .setText("This card has a footer.")
                .setFootnote("I'm the footer!"));

        mCards.add(new CardBuilder(this, CardBuilder.Layout.CAPTION)
                .setText("This card has a puppy background image.")
                .setFootnote("How can you resist?")
                .addImage(R.drawable.puppy_bg));

        mCards.add(new CardBuilder(this, CardBuilder.Layout.COLUMNS)
                .setText("This card has a mosaic of puppies.")
                .setFootnote("Aren't they precious?")
                .addImage(R.drawable.puppy_small_1);
                .addImage(R.drawable.puppy_small_2);
                .addImage(R.drawable.puppy_small_3));
    }

    private class ExampleCardScrollAdapter extends CardScrollAdapter {

        @Override
        public int getPosition(Object item) {
            return mCards.indexOf(item);
        }

        @Override
        public int getCount() {
            return mCards.size();
        }

        @Override
        public Object getItem(int position) {
            return mCards.get(position);
        }

        @Override
        public int getViewTypeCount() {
            return CardBuilder.getViewTypeCount();
        }

        @Override
        public int getItemViewType(int position){
            return mCards.get(position).getItemViewType();
        }

        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
            return mCards.get(position).getView(convertView, parent);
        }
    }
}

Como interagir com cards de rolagem

Como CardScrollView estende AdapterView, é possível implementar os listeners padrão do Android.

  1. Chame o método setOnItemClickListener() herdado no CardScrollView.
  2. Implemente um gerenciador onItemClick() para o evento de toque.

Esta é uma extensão do exemplo anterior que reproduz um som de toque quando você toca em um cartão:

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ...
        setupClickListener();
        setContentView(mCardScrollView);
    }

    private void setupClickListener() {
        mCardScrollView.setOnItemClickListener(new AdapterView.OnItemClickListener() {

            @Override
            public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
                AudioManager am = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
                am.playSoundEffect(Sounds.TAP);
            }
        });
    }

Como animar cards de rolagem

Há três animações disponíveis para cards de rolagem: navegação, inserção e exclusão.

  1. Implemente uma ação de inserção ou exclusão em um card em uma posição específica no conjunto de cards.
  2. Chame animate() e use um valor do tipo enumerado CardScrollView.Animation.
  3. Para exibir uma animação mais suave, remova todas as referências a notifyDataSetChanged(). O método animate() processa a atualização da visualização do conjunto de dados.

    private class ExampleCardScrollAdapter extends CardScrollAdapter {
        ...
    
        // Inserts a card into the adapter, without notifying.
        public void insertCardWithoutNotification(int position, CardBuilder card) {
            mCards.add(position, card);
        }
    }
    
    private void insertNewCard(int position, CardBuilder card) {
        // Insert new card in the adapter, but don't call
        // notifyDataSetChanged() yet. Instead, request proper animation
        // to inserted card from card scroller, which will notify the
        // adapter at the right time during the animation.
        mAdapter.insertCardWithoutNotification(position, card);
        mCardScrollView.animate(position, CardScrollView.Animation.INSERTION);
    }
    

Dicas de performance e implementação para cards de rolagem

Lembre-se das seguintes implicações de design e desempenho ao criar roladores de cards.

Ciclo de vida do cartão

Para melhorar o desempenho, um CardScrollView carrega apenas um subconjunto dos cartões fornecidos por um CardScrollAdapter, que geralmente são visíveis para o usuário, entre outros. Por isso, um cartão pode estar em qualquer um destes quatro estados gerais:

  • Desanexado: a visualização de rolagem do card não precisa desse card no momento. Você vai receber uma notificação do método onDetachedToWindow() se um cartão tiver sido anexado e removido.
  • Attached: a visualização de rolagem do card solicita o card do adaptador com getView(), porque está próximo de ser "ativado". Você recebe uma notificação do método onAttachedToWindow() do cartão quando isso acontece.
  • Ativado: o cartão fica parcialmente visível para o usuário, mas a visualização de rolagem do cartão não "selecionou" o cartão para exibição ao usuário. O método 'isActivated()' retorna true nesse caso.
  • Selecionado: o cartão está ocupando a tela inteira do usuário. Chamar getSelectedView() retorna o cartão selecionado no momento. O método isSelected() retorna verdadeiro nesse caso.

Se você estiver animando a visualização do seu cartão ou fazendo outras operações de alto custo, inicie e interrompa as operações em onAttachedToWindow() e onDetachedToWindow() para economizar recursos.

Reciclagem de cartões

Quando um cartão deixa de ser anexado e é removido, o objeto de visualização associado a ele pode ser reciclado e usado por um cartão que está sendo anexado. Reciclar visualizações com informações atualizadas é muito mais eficiente do que criar novas visualizações.

Para aproveitar a reciclagem de cartões, implemente os métodos getItemViewType(), getViewTypeCount() e getView() da classe CardScrollAdapter. Em seguida, use alguns dos métodos de conveniência na classe CardBuilder para implementar a reciclagem no CardScrollAdapter, como no exemplo a seguir:

private List<CardBuilder> mCards;
...
/**
 * Returns the number of view types for the CardBuilder class. The
 * CardBuilder class has a convenience method that returns this value for
 * you.
 */
@Override
public int getViewTypeCount() {
    return CardBuilder.getViewTypeCount();
}

/**
 * Returns the view type of this card, so the system can figure out
 * if it can be recycled. The CardBuilder.getItemViewType() method
 * returns it's own type.
 */
@Override
public int getItemViewType(int position){
    return mCards.get(position).getItemViewType();
}

/**
 * When requesting a card from the adapter, recycle the view if possible.
 * The CardBuilder.getView() method automatically recycles the convertView
 * it receives, if possible, or creates a new view if convertView is null or
 * of the wrong type.
 */
@Override
public View getView(int position, View convertView, ViewGroup parent) {
    return  mCards.get(position).getView(convertView, parent);
}

Como implementar IDs de cartão estáveis

Quando um cartão é selecionado e está sendo exibido aos usuários, talvez você não queira que as mudanças no adaptador subjacente afetem o cartão mostrado aos usuários nesse momento. Por exemplo, se um usuário estiver visualizando um cartão selecionado e um cartão for removido à esquerda desse cartão, o cartão que o usuário está visualizando pode alternar para a esquerda, porque o CardScrollAdapter reatribui IDs ao conjunto de dados subjacente quando ocorrem mudanças, por padrão.

Se fizer sentido atribuir códigos exclusivos dos seus cartões, é possível manter um ID consistente no conjunto de dados subjacente para evitar o problema mencionado acima. Para fazer isso, substitua hasStableIds() e retorne true. Isso especifica para o sistema que o CardScrollAdapter mantém os IDs estáveis nas mudanças do conjunto de dados. Além disso, implemente getItemId() para retornar o ID exclusivo adequado para os cartões no adaptador. A implementação padrão retorna o índice de posição do cartão no adaptador, que é instável por padrão.

CardScrollAdapter vazio

Quando você tem um conjunto de dados vazio para os adaptadores, a visualização padrão é mostrar uma tela preta. Se você quiser exibir uma visualização diferente nesses casos, não use setEmptyView(). Em vez disso, crie um único card no CardScrollAdapter.

Resposta horizontal

Muitas imersões integradas no Google Glass fornecem feedback "deslocado" ao deslizar para frente e para trás não realizam uma ação. Por exemplo, você pode ver o feedback ao deslizar após tirar uma foto.

Se a imersão não usa gestos de deslizar horizontais para executar funções específicas do app, ofereça esse efeito de embrulho encapsulando o layout em uma CardScrollView que contenha um card.

  1. Copie a seguinte classe auxiliar no seu projeto:

    public class TuggableView extends CardScrollView {
    
        private final View mContentView;
    
        /**
         * Initializes a TuggableView that uses the specified layout
         * resource for its user interface.
         */
        public TuggableView(Context context, int layoutResId) {
            this(context, LayoutInflater.from(context)
                    .inflate(layoutResId, null));
        }
    
        /**
         * Initializes a TuggableView that uses the specified view
         * for its user interface.
         */
        public TuggableView(Context context, View view) {
            super(context);
    
            mContentView = view;
            setAdapter(new SingleCardAdapter());
            activate();
        }
    
        /**
         * Overridden to return false so that all motion events still
         * bubble up to the activity's onGenericMotionEvent() method after
         * they are handled by the card scroller. This allows the activity
         * to handle TAP gestures using a GestureDetector instead of the
         * card scroller's OnItemClickedListener.
         */
        @Override
        protected boolean dispatchGenericFocusedEvent(MotionEvent event) {
            super.dispatchGenericFocusedEvent(event);
            return false;
        }
    
        /** Holds the single "card" inside the card scroll view. */
        private class SingleCardAdapter extends CardScrollAdapter {
    
            @Override
            public int getPosition(Object item) {
                return 0;
            }
    
            @Override
            public int getCount() {
                return 1;
            }
    
            @Override
            public Object getItem(int position) {
                return mContentView;
            }
    
            @Override
            public View getView(int position, View recycleView,
                    ViewGroup parent) {
                return mContentView;
            }
        }
    }
    
  2. Modifique o método onCreate na sua atividade para exibir o CardScrollView que contém o layout.

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
    
        // was: setContentView(R.layout.main_activity);
        setContentView(new TuggableView(this, R.layout.main_activity));
    }