Glass를 사용하면 스크롤 및 애니메이션과 같은 카드와의 풍부한 상호작용을 빌드할 수 있습니다.
활동에서 스크롤 카드
Glass 디스플레이와 터치패드는 Glass 타임라인과 같이 스와이프 가능한 카드를 표시하는 데 적합합니다. 활동을 빌드하는 경우 CardScrollView
위젯을 사용하여 동일한 유형의 효과를 만들 수 있습니다.
CardScrollAdapter
를 구현하여 카드를CardScrollView
에 제공합니다. 표준 뷰 계층 구조를 직접 빌드하거나CardBuilder
클래스를 사용할 수 있습니다.CardScrollAdapter
를 카드 공급업체로 사용하는CardScrollView
를 만듭니다.- 활동의 콘텐츠 뷰를
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);
}
}
}
스크롤 카드와 상호작용
CardScrollView
는 AdapterView
를 확장하므로 표준 Android 리스너를 구현할 수 있습니다.
CardScrollView
에서 상속된setOnItemClickListener()
를 호출합니다.- 탭 이벤트의
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);
}
});
}
스크롤 카드 애니메이션
스크롤 카드에는 탐색, 삽입, 삭제의 세 가지 애니메이션이 있습니다.
- 카드 세트의 특정 위치에 카드에 삽입 또는 삭제 작업을 구현합니다.
animate()
를 호출하고CardScrollView.Animation
enum의 값을 사용합니다.더 매끄러운 애니메이션을 표시하려면
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); }
카드 스크롤 성능 및 구현 팁
카드 스크롤러를 만들 때는 다음과 같은 디자인 및 성능에 미치는 영향을 기억하세요.
카드 수명 주기
성능을 높이기 위해 CardScrollView
는 CardScrollAdapter
에서 제공하는 카드의 하위 집합 (일반적으로 사용자에게 표시되는 카드 및 일부 추가 카드)만 로드합니다.
따라서 카드는 다음 네 가지 일반 상태 중 하나일 수 있습니다.
- 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
내부에 레이아웃을 래핑하여 이 조절 효과를 제공하세요.
다음 도우미 클래스를 프로젝트에 복사합니다.
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; } } }
활동의
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)); }