카드 스크롤러

Glass를 사용하면 스크롤 및 애니메이션과 같은 카드와의 풍부한 상호작용을 빌드할 수 있습니다.

활동에서 스크롤 카드

Glass 디스플레이와 터치패드는 Glass 타임라인과 같이 스와이프 가능한 카드를 표시하는 데 적합합니다. 활동을 빌드하는 경우 CardScrollView 위젯을 사용하여 동일한 유형의 효과를 만들 수 있습니다.

  1. CardScrollAdapter를 구현하여 카드를 CardScrollView에 제공합니다. 표준 뷰 계층 구조를 직접 빌드하거나 CardBuilder 클래스를 사용할 수 있습니다.
  2. CardScrollAdapter를 카드 공급업체로 사용하는 CardScrollView를 만듭니다.
  3. 활동의 콘텐츠 뷰를 CardScrollView로 설정하거나 레이아웃에 CardScrollView를 표시합니다.

다음은 세 개의 카드를 스크롤하는 간단한 구현입니다.

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);
        }
    }
}

스크롤 카드와 상호작용

CardScrollViewAdapterView를 확장하므로 표준 Android 리스너를 구현할 수 있습니다.

  1. CardScrollView에서 상속된 setOnItemClickListener()를 호출합니다.
  2. 탭 이벤트의 onItemClick() 핸들러를 구현합니다.

다음은 카드를 탭할 때 탭 사운드를 재생하는 이전 예의 확장 프로그램입니다.

    @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);
            }
        });
    }

스크롤 카드 애니메이션

스크롤 카드에는 탐색, 삽입, 삭제의 세 가지 애니메이션이 있습니다.

  1. 카드 세트의 특정 위치에 카드에 삽입 또는 삭제 작업을 구현합니다.
  2. animate()를 호출하고 CardScrollView.Animation enum의 값을 사용합니다.
  3. 더 매끄러운 애니메이션을 표시하려면 notifyDataSetChanged() 참조를 삭제합니다. animate() 메서드는 데이터 세트 뷰 업데이트를 처리합니다.

    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);
    }
    

카드 스크롤 성능 및 구현 팁

카드 스크롤러를 만들 때는 다음과 같은 디자인 및 성능에 미치는 영향을 기억하세요.

카드 수명 주기

성능을 높이기 위해 CardScrollViewCardScrollAdapter에서 제공하는 카드의 하위 집합 (일반적으로 사용자에게 표시되는 카드 및 일부 추가 카드)만 로드합니다. 따라서 카드는 다음 네 가지 일반 상태 중 하나일 수 있습니다.

  • Detached(분리형) - 카드 스크롤 뷰에 현재 이 카드가 필요하지 않습니다. 카드가 이전에 연결되었다가 분리되면 카드의 onDetachedToWindow() 메서드를 통해 알림을 받습니다.
  • 연결됨 - 카드가 '활성화'에 가까워졌으므로 카드 스크롤 뷰에서 어댑터에 getView()를 사용하여 카드를 요청합니다. 이 경우 카드의 onAttachedToWindow() 메서드를 통해 알림이 전송됩니다.
  • 활성화됨 - 카드가 사용자에게 부분적으로 표시되지만 카드 스크롤 뷰가 카드를 '선택'하지 않아 사용자에게 표시할 수 없습니다. 이 경우 'isActivated()' 메서드가 true를 반환합니다.
  • 선택됨 - 카드가 사용자의 전체 화면을 사용하고 있습니다. getSelectedView()를 호출하면 현재 선택한 카드가 반환됩니다. 이 경우 isSelected() 메서드는 true를 반환합니다.

카드 뷰를 애니메이션 처리하거나 비용이 많이 드는 다른 작업을 하는 경우 onAttachedToWindow()onDetachedToWindow()에서 작업을 시작 및 중지하여 리소스를 저장합니다.

카드 재활용

카드가 분리되어 분리되면 카드와 연결된 뷰 객체를 재활용하고 연결된 카드에서 사용할 수 있습니다. 업데이트된 정보로 뷰를 재활용하는 것이 새 뷰를 만드는 것보다 훨씬 더 효율적입니다.

카드 재활용을 활용하려면 CardScrollAdapter 클래스의 getItemViewType(), getViewTypeCount(), getView() 메서드를 구현합니다. 그런 다음 CardBuilder 클래스의 편의 메서드를 사용하여 다음 예와 같이 CardScrollAdapter에서 재활용을 구현합니다.

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);
}

안정적인 카드 ID 구현

카드가 선택되어 사용자에게 표시될 때 기본 어댑터 변경사항이 현재 사용자에게 표시되는 카드에 영향을 주지 않도록 할 수도 있습니다. 예를 들어 사용자가 선택한 카드를 볼 때 카드가 왼쪽에 표시되는 경우, 기본적으로 CardScrollAdapter에서 ID가 변경될 때 기본 데이터 세트에 ID를 다시 할당하므로 사용자가 보고 있는 카드가 왼쪽으로 이동할 수 있습니다.

카드에 고유한 ID를 할당하는 것이 논리적이라면 앞서 언급한 문제를 방지하기 위해 기본 데이터 세트에서 일관된 ID를 유지할 수 있습니다. 이렇게 하려면 hasStableIds()를 재정의하고 true를 반환합니다. 이는 CardScrollAdapter가 데이터 세트 변경사항에서 안정적인 ID를 유지하도록 시스템에 지정합니다. 또한 getItemId()를 구현하여 어댑터의 카드에 적절한 고유 ID를 반환합니다. 기본 구현은 기본적으로 어댑터에 있는 카드의 위치 색인을 반환합니다. 본질적으로 불안정합니다.

CardScrollAdapter가 비어 있습니다.

어댑터에 빈 데이터 세트가 있는 경우 기본 뷰는 검은색 화면을 표시합니다. 이러한 경우 다른 뷰를 표시하려면 setEmptyView()를 사용하지 마세요. 대신 CardScrollAdapter에서 단일 카드를 만드세요.

가로 방향 견인 피드백

Glass에 내장된 몰입감으로 앞뒤로 스와이프해도 작업이 실행되지 않을 때 '너무 높은' 피드백을 제공합니다. 예를 들어 사진을 찍은 후 스와이프할 때 이 의견을 볼 수 있습니다.

몰입이 수평 스와이프 동작을 사용하여 애플리케이션 관련 기능을 실행하지 않는다면 하나의 카드가 포함된 CardScrollView 내부에 레이아웃을 래핑하여 이 조절 효과를 제공하세요.

  1. 다음 도우미 클래스를 프로젝트에 복사합니다.

    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. 활동의 onCreate 메서드를 수정하여 레이아웃이 포함된 CardScrollView를 표시합니다.

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