본문으로 건너뛰기

캐러셀 구현

BuzzAd Android용 SDK v3.5.x부터 추가된 캐러셀(Carousel)은 여러 개의 광고를 가로로 스크롤 하여 사용자에게 노출할 수 있는 UI입니다. BuzzAd Android용 SDK를 사용하여 Native 지면에 캐러셀을 생성하고 필수 기능을 구현해 캐러셀을 구현할 수 있습니다. 또한 선택에 따라 구현할 수 있는 부가 기능들을 통해 사용자 경험을 최적화할 수도 있습니다.

 중요
이 기능은 BuzzAd Android용 SDK v3.5.x부터 추가할 수 있습니다. v3.5.x 미만의 버전을 연동한 경우 이 기능을 탑재하려면 v3.5.x 이상으로 업데이트하세요.

✏️  참고

  • 모든 예제는 Java로 제공합니다.
  • 여기에서는 전체 코드에서 연동에 필요한 코드 예제만을 제공합니다. 전체 코드를 확인하려면 샘플 앱을 참고하세요.

캐러셀 구현하기

BuzzAd Android용 SDK에서는 아이템 뷰를 화면에 가로 레이아웃으로 표시하는 RecyclerView와 이를 처리하는 어댑터를 사용하여 네이티브 지면에 캐러셀을 구현할 수 있습니다.

RecyclerView 생성하기

RecyclerView를 생성하려면 다음의 절차를 따르세요.

  1. 캐러셀을 넣고 싶은 영역에 RecyclerView를 추가한 액티비티 레이아웃 파일(activity_native_carousel.xml)을 구현하세요.

    <!-- activity_native_carousel.xml -->
    ....
    <androidx.recyclerview.widget.RecyclerView
    android:id="@+id/carousel"
    android:layout_width="match_parent"
    android:layout_height="wrap_content" />
    ....
  2. 뷰 레이아웃 파일(view_item_carousel.xml)에 RecyclerView의 각 아이템에 대한 레이아웃을 작성하세요.

    ✏️  참고
    샘플 앱view_item_carousel.xml 파일을 참고해서 작성할 수도 있습니다.

    <!-- view_item_carousel.xml -->
    <?xml version="1.0" encoding="utf-8"?>
    <com.buzzvil.buzzad.benefit.presentation.nativead.NativeAdView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">
    ....
    </com.buzzvil.buzzad.benefit.presentation.nativead.NativeAdView>
  3. RecyclerView의 각 아이템을 담당할 CarouselItem 클래스를 선언하세요.

    ✏️  참고
    RecyclerView에는 피드 지면으로의 진입점도 추가할 수 있습니다. 그러므로 확장성을 고려하여 CarouselItem 을 기반 클래스로 선언하고, 실제 각 아이템은 이를 상속하여 선언하는 것을 권장합니다. 피드 지면으로의 진입점을 추가하는 방법에 대한 자세한 내용은 피드 엔트리 포인트를 참고하세요.

    다음은 기반 클래스인 CarouselItem을 선언하고, 이를 확장해서 광고 객체를 들고 있는 NativeAdItem 클래스를 선언하는 예시입니다.

    class CarouselItem {
    // NativeAd를 가지고 있는 아이템
    static class NativeAdItem extends CarouselItem {
    public final NativeAd nativeAd;

    NativeAdItem(final NativeAd nativeAd) {
    this.nativeAd = nativeAd;
    }
    }
    }
  4. RecyclerView에 들어갈 아이템이 다음과 같이 생성되는지 확인하세요.

    val newItem = new CarouselItem.NativeAdItem(nativeAd);

RecyclerView 어댑터 구현하기

RecyclerView 어댑터를 구현하려면 캐러셀을 처리하기 위한 어댑터와 어댑터 내의 CarouselDiffViewHolder를 구현해야 합니다. 다음의 절차를 따르세요.

  1. 캐러셀을 처리하기 위한 어댑터(CarouselAdapter)를 구현하세요. ListAdapterRecyclerView.Adapter를 확장하여 어댑터를 구현할 수 있습니다.

    다음은 ListAdapter를 확장하여 구현하는 예시입니다.

    ✏️  참고

    • 일반적인 ListAdapter를 구현하는 방식과 크게 다르지 않으며, ViewHolder의 생성과 Bind, Recycle할 때의 구현만 추가됩니다.
    • 예시에 사용된 뷰 결합(View Binding)은 필수 요소가 아닙니다. 뷰 결합에 대한 자세한 내용은 Android 공식 문서를 참고하세요.

    ⚠️ 주의
    광고의 상태가 여러 뷰와 섞이지 않도록 반드시 onViewRecycled()에서 unbind()를 호출하세요.

    class CarouselAdapter extends ListAdapter<CarouselItem, CarouselAdapter.ViewHolder> {
    public CarouselAdapter() {
    super(new CarouselDiff());
    }

    @Override
    public CarouselAdapter.ViewHolder onCreateViewHolder(
    ViewGroup parent,
    int viewType
    ) {
    return new ViewHolder(ViewItemCarouselBinding.inflate(LayoutInflater.from(parent.getContext())));
    }

    @Override
    public void onBindViewHolder(final CarouselAdapter.ViewHolder holder, final int position) {
    final CarouselItem item = getItem(position);
    if (item instanceof CarouselItem.NativeAdItem) {
    holder.bind(((CarouselItem.NativeAdItem) item).nativeAd);
    }
    }

    @Override
    public void onViewRecycled(final ViewHolder holder) {
    super.onViewRecycled(holder);
    // unbind를 반드시 호출하여 NativeAd나 FeedPromotion을 재사용할 때 문제가 발생하지 않게 합니다.
    holder.unbind();
    }

    private static class CarouselDiff extends DiffUtil.ItemCallback<CarouselItem> {
    // TODO
    }

    protected static class ViewHolder extends RecyclerView.ViewHolder {
    // TODO
    }
    }
  2. 어댑터에 대한 업데이트를 콜백하는 하위 클래스 CarouselDiff를 구현하세요.

    ✏️  참고
    아래 예시는 이해를 돕기 위해 실제 제품과 관계없이 가장 기본적인 구현 방식을 제시합니다. 실제로 구현할 때에는 환경에 맞게 올바른 동일성 체크 로직을 구현하시기 바랍니다.

    private static class CarouselDiff extends DiffUtil.ItemCallback<CarouselItem> {
    @Override
    public boolean areItemsTheSame(final CarouselItem oldItem, final CarouselItem newItem) {
    if (oldItem instanceof CarouselItem.NativeAdItem
    && newItem instanceof CarouselItem.NativeAdItem
    && ((CarouselItem.NativeAdItem) oldItem).nativeAd == ((CarouselItem.NativeAdItem) newItem).nativeAd) {
    return true;
    } else if (oldItem instanceof CarouselItem.CarouselToFeedSlideItem
    && newItem instanceof CarouselItem.CarouselToFeedSlideItem) {
    return true;
    }
    return false;
    }

    @Override
    public boolean areContentsTheSame(final CarouselItem oldItem, final CarouselItem newItem) {
    if (oldItem instanceof CarouselItem.NativeAdItem
    && newItem instanceof CarouselItem.NativeAdItem
    && ((CarouselItem.NativeAdItem) oldItem).nativeAd.getId() == ((CarouselItem.NativeAdItem) newItem).nativeAd.getId()) {
    return true;
    } else if (oldItem instanceof CarouselItem.CarouselToFeedSlideItem
    && newItem instanceof CarouselItem.CarouselToFeedSlideItem) {
    return true;
    }
    return false;
    }
    }
  3. 레이아웃에 정의한 각 뷰에 NativeAd의 요소가 정상적으로 설정될 수 있도록 하위 클래스인 ViewHolder를 구현하세요.

  • ViewHolderNativeAdViewBinder를 이용합니다.

    protected static class ViewHolder extends RecyclerView.ViewHolder {

    // 레이아웃과 NativeAd를 연결해주는 ViewBinder
    private final NativeAdViewBinder nativeAdViewBinder;

    public ViewHolder(final ViewItemCarouselBinding binding) {
    super(binding.getRoot());

    nativeAdViewBinder = new NativeAdViewBinder.Builder(
    binding.getRoot(),
    binding.adMediaView
    )
    .titleTextView(binding.adTitleText)
    .descriptionTextView(binding.adDescriptionText)
    .iconImageView(binding.adIconImage)
    .ctaView(binding.adCtaView)
    .build();
    }

    /**
    * 레이아웃과 NativeAd를 연결합니다.
    *
    * @param nativeAd: 레이아웃에 연결할 NativeAd 객체
    */
    void bind(final NativeAd nativeAd) {
    nativeAdViewBinder.bind(nativeAd);
    }

    /**
    * 레이아웃과 NativeAd 간의 연결을 해제합니다.
    */
    void unbind() {
    this.nativeAdViewBinder.unbind();
    }
    }

액티비티 또는 프래그먼트에 결합하기

생성을 완료한 RecyclerView와 어댑터를 액티비티 혹은 프래그먼트에 bind하여 사용할 수 있습니다. 다음은 RecyclerView와 어댑터를 액티비티(NativeCarouselActivity)에 bind하는 예시입니다.

/**
* BuzzAd SDK를 사용하여 Carousel UI를 구현하는 방법에 대한 예제 코드
*/
public class NativeCarouselActivity extends AppCompatActivity {
private ActivityNativeCarouselBinding binding;
private CarouselAdapter adapter;

@Override
public void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivityNativeCarouselBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
adapter = new CarouselAdapter();
}
}

액티비티에 RecyclerView와 어댑터를 결합하려면 다음의 절차를 따르세요.

  1. 캐러셀 초기화 함수를 호출한 후 RecyclerView에 CarouselAdapter를 할당하세요.
  • PagerSnapHelper: 캐러셀의 스크롤이 중간에 멈추지 않고 각 광고 아이템이 온전히 노출되는 방향으로 자연스럽게 동작하게 됩니다. PagerSnapHelper에 대한 자세한 내용은 Android 공식 문서를 참고하세요.

    public class NativeCarouselActivity extends AppCompatActivity {
    ....
    @Override
    public void onCreate(final Bundle savedInstanceState) {
    ....
    initCarousel();
    }

    private void initCarousel() {
    binding.carousel.setAdapter(adapter);

    // ViewPager와 비슷하게, 스크롤이 중간에서 멈추면 한쪽 아이템을 선택하여 다 보일 때까지 스크롤
    new PagerSnapHelper().attachToRecyclerView(binding.carousel);
    }
    }
  1. RecyclerView에 LayoutManager(LinearLayoutManager)를 할당한 후 광고 아이템 배치 방향 파라미터를 HORIZONTAL로 설정하세요. 그런 다음, checkLayoutParams에서 각 아이템이 화면 폭 전체를 차지하도록 설정하세요.

    public class NativeCarouselActivity extends AppCompatActivity {
    ....
    // LinearLayoutManager를 사용하고 HORIZONTAL로 설정하여 수평으로 일렬로 정렬된 레이아웃을 사용합니다.
    private final LinearLayoutManager layoutManager = new LinearLayoutManager(
    this,
    HORIZONTAL,
    false
    ) {
    // Carousel 아이템의 폭이 화면을 채우도록 변경합니다.
    @Override
    public boolean checkLayoutParams(final RecyclerView.LayoutParams lp) {
    lp.width = getWidth();
    return true;
    }
    };
    ....
    private void initCarousel() {
    ....
    binding.carousel.setLayoutManager(layoutManager);
    ....
    }
    }
  2. 광고를 요청하는 코드를 호출하세요.

  • onError(): 광고 로딩 오류가 발생할 때 호출됩니다.

  • onEmpty(): 노출할 수 있는 광고가 없을 때 호출됩니다.

  • onAdsLoaded(): 요청할 수 있는 광고가 있을 때 호출됩니다.

    public class NativeCarouselActivity extends AppCompatActivity {
    ....
    // Carousel 지면에서 사용할 Unit ID
    private final String unitId = App.UNIT_ID_NATIVE_AD;
    ....
    private final NativeAdLoader.OnAdsLoadedListener adsLoadedListener = new NativeAdLoader.OnAdsLoadedListener() {

    @Override
    public void onLoadError(final AdError error) {
    onError();
    }

    @Override
    public void onAdsLoaded(final Collection<NativeAd> nativeAds) {
    if (nativeAds.isEmpty()) {
    onEmpty();
    } else {
    NativeCarouselActivity.this.onAdsLoaded(nativeAds);
    }
    }
    };
    ...
    @Override
    public void onCreate(final Bundle savedInstanceState) {
    ....
    loadAds();
    }
    ....
    private void loadAds() {
    new NativeAdLoader(unitId).loadAds(
    adsLoadedListener,
    new NativeAdLoaderParams.Builder()
    .count(5)
    .build()
    );
    }
    }
  1. 광고 요청 결과에 따른 동작을 구현하세요.

    public class NativeCarouselActivity extends AppCompatActivity {
    ....
    private void onError() {
    // 오류가 발생하면 지면을 표시하지 않습니다.
    binding.placement.setVisibility(View.GONE);
    Log.d(TAG, "An error occurred while loading ads");
    }

    private void onEmpty() {
    // 보여줄 광고가 없으면 지면을 표시하지 않습니다.
    binding.placement.setVisibility(View.GONE);
    Log.d(TAG, "The ad list is empty");
    }

    private void onAdsLoaded(final Collection<NativeAd> nativeAds) {
    binding.placement.setVisibility(View.VISIBLE);
    final List<CarouselItem> items = buildCarouselItems(nativeAds);
    adapter.submitList(items);
    Log.d(TAG, "" + nativeAds.size() + " Ads are loaded");
    }
    }
  2. NativeAd 객체를 CarouselItem으로 변경하는 코드를 작성하세요.

    public class NativeCarouselActivity extends AppCompatActivity {
    ....
    /**
    * NativeAd 배열을 Carousel에 채워넣을 CarouselItem으로 변환하는 함수
    *
    * @param nativeAds: Carousel에 표시할 광고 객체 배열
    */
    private List<CarouselItem> buildCarouselItems(final Collection<NativeAd> nativeAds) {
    List<CarouselItem> items = new ArrayList<>();
    for (final NativeAd nativeAd : nativeAds) {
    items.add(new CarouselItem.NativeAdItem(nativeAd));
    }
    return items;
    }
    }

캐러셀의 필수 기능 구현하기

BuzzAd Android용 SDK는 다양한 광고 및 사용자가 취향에 맞게 광고를 필터링할 수 있는 피드 지면 진입 경로인 슬라이드 페이지와 엔트리 포인트를 제공합니다.

 중요
기존에 피드 지면을 연동하고 있거나 연동할 예정이 있는 경우, SDK 연동의 효용성을 극대화하기 위해 피드 진입 슬라이드 및 피드 엔트리 포인트를 반드시 구현하시기 바랍니다.

피드 진입 슬라이드

캐러셀에서 피드로 진입할 수 있는 UI인 슬라이드(Carousel To Feed Slide)를 캐러셀의 마지막 페이지에 구현해야 합니다. 이를 위해 BuzzAd Android용 SDK는 아래 표에 나열된 클래스를 제공합니다.

코드설명
FeedPromotion피드 진입 슬라이드에 표시할 리소스(이미지, 문구)를 가지고 있는 객체입니다.
FeedPromotionFactoryFeedPromotion 객체를 생성합니다.
  • buildForCarousel(): 캐러셀에 맞는 리소스를 할당하여 FeedPromotion 객체를 생성합니다.
FeedPromotionViewBinderNativeAdViewFeedPromotion을 보여줄 수 있도록 연결합니다.
  • bind(): FeedPromotionNativeAdView의 관계를 연결합니다.
  • unbind(): FeedPromotionNativeAdView의 관계를 끊습니다.
  • Builder: FeedPromotionViewBinder 객체를 생성할 때 사용하는 빌더입니다.

피드 진입 슬라이드를 구현하려면 다음의 절차를 따르세요.

  1. CarouselItem에서 피드 진입 슬라이드를 보여주기 위한 클래스의 선언을 추가하세요. 다음은 기존의 CarouselItem 구현에서 CarouselToFeedSlideItem 클래스의 선언을 추가하는 예시입니다.

    ```java
    class CarouselItem {
    // NativeAd를 가지고 있는 아이템
    static class NativeAdItem extends CarouselItem {
    public final NativeAd nativeAd;

    NativeAdItem(final NativeAd nativeAd) {
    this.nativeAd = nativeAd;
    }
    }

    // Carousel To Feed Slide를 가지고 있는 아이템
    static class CarouselToFeedSlideItem extends CarouselItem {
    public final FeedPromotion feedPromotion;

    CarouselToFeedSlideItem(final FeedPromotion feedPromotion) {
    this.feedPromotion = feedPromotion;
    }
    }
    }

    ```
  2. CarouselToFeedSlideItem에 맞는 동작을 구현하기 위해 어댑터(CarouselAdapter)를 구현하세요.

    i. onBindeViewHolder에서 CarouselToFeedSlideItem일 때 호출할 bind 함수를 연결하세요.

    class CarouselAdapter extends ListAdapter<CarouselItem, CarouselAdapter.ViewHolder> {
    ....
    @Override
    public void onBindViewHolder(final CarouselAdapter.ViewHolder holder, final int position) {
    final CarouselItem item = getItem(position);
    if (item instanceof CarouselItem.NativeAdItem) {
    holder.bind(((CarouselItem.NativeAdItem) item).nativeAd);
    } else if (item instanceof CarouselItem.CarouselToFeedSlideItem) {
    holder.bind(((CarouselItem.CarouselToFeedSlideItem) item).feedPromotion);
    }
    }
    ....
    }

    ii. ViewHolder를 구현하세요. 구현 시 FeedPromotion을 받아서 FeedPromotionViewBinder를 이용해 NativeAdView에 각 요소가 잘 연결될 수 있도록 해야 합니다.
    또한 기존에 작성한 코드에서 NativeAdViewBinder와 비슷하게 객체를 생성한 후 bind 함수를 만든 다음 unbind()를 호출하도록 코드를 추가하세요.

    class CarouselAdapter extends ListAdapter<CarouselItem, CarouselAdapter.ViewHolder> {
    ....
    protected static class ViewHolder extends RecyclerView.ViewHolder {
    // 레이아웃과 NativeAd를 연결해주는 ViewBinder
    private final NativeAdViewBinder nativeAdViewBinder;

    // 레이아웃과 FeedPromotion을 연결해주는 ViewBinder
    private final FeedPromotionViewBinder carouselToFeedSlideViewBinder;

    public ViewHolder(final ViewItemCarouselBinding binding) {
    super(binding.getRoot());

    nativeAdViewBinder = new NativeAdViewBinder.Builder(
    binding.getRoot(),
    binding.adMediaView
    )
    .titleTextView(binding.adTitleText)
    .descriptionTextView(binding.adDescriptionText)
    .iconImageView(binding.adIconImage)
    .ctaView(binding.adCtaView)
    .build();

    carouselToFeedSlideViewBinder = new FeedPromotionViewBinder.Builder(
    binding.getRoot(),
    binding.adMediaView
    )
    .titleTextView(binding.adTitleText)
    .descriptionTextView(binding.adDescriptionText)
    .iconImageView(binding.adIconImage)
    .ctaView(binding.adCtaView)
    .build();
    }
    ....
    /**
    * 레이아웃과 FeedPromotion을 연결하여 Carousel To Feed Slide를 만듭니다.
    *
    * @param feedPromotion: 레이아웃에 연결할 FeedPromotion 객체
    */
    void bind(final FeedPromotion feedPromotion) {
    carouselToFeedSlideViewBinder.bind(feedPromotion);
    }

    /**
    * 레이아웃과 NativeAd / FeedPromotion 간의 연결을 해제
    */
    fun unbind() {
    this.nativeAdViewBinder.unbind();
    this.carouselToFeedSlideViewBinder.unbind();
    }
    }
    }

    iii. 데이터에 FeedPromotion이 있으면 UI에 정상적으로 표시되는지 확인하세요.

  3. 액티비티(NativeCarouselActivity)를 구현하세요. 구현 시 광고 데이터를 어댑터에 할당하는 시점을 기준으로 광고가 있는 경우, 마지막에 FeedPromotion을 붙여 넣도록 구현해야 합니다. 아래 코드의 if 블록을 참고하세요.

    public class NativeCarouselActivity extends AppCompatActivity {
    ....
    /**
    * NativeAd 배열을 Carousel에 채워넣을 CarouselItem으로 변환하는 함수
    *
    * @param nativeAds: Carousel에 표시할 광고 객체 배열
    *
    * 광고가 비어있지 않으면 마지막 페이지에 Carousel To Feed Slide를 추가합니다
    */
    private List<CarouselItem> buildCarouselItems(final Collection<NativeAd> nativeAds) {
    List<CarouselItem> items = new ArrayList<>();
    for (final NativeAd nativeAd : nativeAds) {
    items.add(new CarouselItem.NativeAdItem(nativeAd));
    }
    if (!items.isEmpty()) {
    items.add(new CarouselItem.CarouselToFeedSlideItem(
    new FeedPromotionFactory(unitId).buildForCarousel()
    ));
    }
    return items;
    }
    }

피드 엔트리 포인트

캐러셀에서 피드로 진입할 수 있는 UI인 엔트리 포인트(Carousel To Feed Link)를 캐러셀의 하단에 구현해야 합니다.

피드 엔트리 포인트를 구현하려면 다음의 절차를 따르세요.

  1. 캐러셀로 사용할 RecyclerView 아래에 FeedEntryView를 추가하세요.

     중요
    app:buzzvilFeedEntryViewName은 아래 예제 코드처럼 "carousel_to_feed_link"로 설정하세요.

    ....
    <!-- 광고를 표시할 Carousel -->
    <!-- clipToPadding, paddingHorizontal: 이전과 이후 아이템을 살짝 보여주기 위해 설정 -->
    <androidx.recyclerview.widget.RecyclerView
    android:id="@+id/carousel"
    ....
    />

    <!-- Carousel To Feed Link -->
    <!-- Carousel 하단에서 Feed를 열 수 있게 하는 링크 -->
    <!-- buzzvilFeedEntryViewName의 값은 여기 예시와 같은 값을 사용 -->
    <com.buzzvil.buzzad.benefit.presentation.feed.entrypoint.FeedEntryView
    android:id="@+id/toFeedLink"
    app:buzzvilFeedEntryViewName="carousel_to_feed_link"
    ....>
    <!-- 자유롭게 뷰 구현 -->
    </com.buzzvil.buzzad.benefit.presentation.feed.entrypoint.FeedEntryView>
  2. FeedEntryView를 구현하세요. 다음은 간단한 텍스트와 아이콘을 추가한 예시입니다.

    <com.buzzvil.buzzad.benefit.presentation.feed.entrypoint.FeedEntryView
    app:buzzvilFeedEntryViewName="carousel_to_feed_link"
    ....>
    <LinearLayout
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:orientation="horizontal">

    <TextView
    style="@style/NativeCarouselToFeedLink"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="포인트 더 받으러 가기" />

    <ImageView
    android:layout_width="16dp"
    android:layout_height="16dp"
    android:scaleType="centerInside"
    android:src="@drawable/ic_chevron_right" />
    </LinearLayout>
    </com.buzzvil.buzzad.benefit.presentation.feed.entrypoint.FeedEntryView>
  3. 광고 로딩 요청에 대한 응답 상태에 따른 피드 엔트리 포인트의 표시 여부를 설정하세요.

  • 캐러셀 및 피드 엔트리 포인트 미표시: 광고가 아직 로드되지 않은 경우, 오류가 발생하는 경우, 광고가 없는 경우

  • 캐러셀 및 피드 엔트리 포인트 표시: 보여줄 광고가 있는 경우

    public class NativeCarouselActivity extends AppCompatActivity {
    ....
    private void onError() {
    // 오류가 발생하면 지면을 표시하지 않습니다
    binding.placement.setVisibility(View.GONE);
    ....
    }

    private void onEmpty() {
    // 보여줄 광고가 없으면 지면을 표시하지 않습니다
    binding.placement.setVisibility(View.GONE);
    ....
    }

    private void onAdsLoaded(final Collection<NativeAd> nativeAds) {
    binding.placement.setVisibility(View.VISIBLE);
    ....
    }
    }

    ✏️  참고
    샘플 앱에서는 다음과 같은 Constraint Layout의 Group 위젯을 사용하여 두 뷰의 상태를 동시에 관리합니다. Group 위젯에 대한 자세한 내용은 Android 공식 문서를 참고하세요.

    ....
    <!-- carousel과 toFeedLink의 visibility를 한 번에 다루기 위한 Group -->
    <androidx.constraintlayout.widget.Group
    android:id="@+id/placement"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:visibility="gone"
    app:constraint_referenced_ids="carousel,toFeedLink" />
    ....

광고 아이템의 높이 차이와 상관 없이 피드 엔트리 포인트 위치 고정하기

각 광고 아이템의 설명 텍스트(ad_description_text) 길이 차이로 인해 라인 수가 상이하면 각 슬라이드의 피드 엔트리 포인트의 위치가 계속 변경되는 현상이 발생할 수 있습니다. 이러한 현상의 발생을 방지하기 위해 광고 설명 텍스트를 표시하는 뷰의 lines 값을 고정하여 항상 일정한 위치를 유지할 수 있습니다.

다음은 광고 설명 텍스트를 표시하는 뷰의 lines 값을 2로 설정한 예시입니다.

<!-- view_item_carousel.xml -->
....
<TextView
android:id="@+id/ad_description_text"
android:ellipsize="end"
android:lines="2"
....
/>

캐러셀의 부가 기능 구현하기

⚠️ 주의
이 가이드는 모든 부가 기능을 적용한 상태로 작성되었습니다. 각각의 부가 기능은 서로 영향을 줄 수 있기 때문에, 일부만 적용할 경우 UI가 비정상적으로 표시될 수 있습니다. (예: 무한 루프 기능과 아이템 폭 줄임만 적용하는 경우, 첫 아이템의 이전 아이템이 보이지 않는 문제가 발생합니다.)
이러한 문제가 발생하는 경우에는 각자의 환경에 맞게 문제를 해결하는 것을 권장합니다.

무한 루프 구현하기

캐러셀에서 유한한 아이템을 끊임 없이 스크롤할 수 있는 무한 루프 기능을 구현할 수 있습니다. 캐러셀의 무한 루프를 구현하는 방법는 매우 다양하나, 이 가이드에서는 Adapter의 getItemCount()가 매우 큰 값을 반환하게 하는 가장 간단한 방법을 제시합니다.

무한 루프를 구현하려면 다음의 절차를 따르세요.

  1. 어댑터(CarouselAdapter)가 파라미터를 받도록 변경하세요.

    class CarouselAdapter extends ListAdapter<CarouselItem, CarouselAdapter.ViewHolder> {
    private final boolean isInfiniteLoopEnabled;
    ....
    public CarouselAdapter(final boolean isInfiniteLoopEnabled) {
    super(new CarouselDiff());
    this.isInfiniteLoopEnabled = isInfiniteLoopEnabled;
    }
    ....
    }
  2. getItemCount()getItem()의 구현을 변경하세요.

  • getItemCount(): RecyclerView가 무한한 데이터를 가진 것처럼 표시되도록, isInfiniteLoopEnabledtrue이고 보여줄 아이템이 있는 경우에는 Integer.MAX_VALUE를 반환하도록 변경하세요.

  • getItem(): super.getItemCount()를 통해 얻은 실제 데이터 값으로 무한 루프의 position을 나누어 실제 index 값을 획득하도록 변경하세요.

    class CarouselAdapter extends ListAdapter<CarouselItem, CarouselAdapter.ViewHolder> {
    ....
    @Override
    public int getItemCount() {
    final int actualItemCount = super.getItemCount();
    if (isInfiniteLoopEnabled && actualItemCount > 0) {
    // 무한 루프를 쉽게 구현하는 방법으로 매우 큰 수를 여기서 반환합니다.
    return Integer.MAX_VALUE;
    }
    return actualItemCount;
    }

    @Override
    public CarouselItem getItem(final int position) {
    int newPosition;
    if (isInfiniteLoopEnabled) {
    // 무한 루프인 경우의 position은 매우 큰 수이므로 실제 아이템 수로 나눈 나머지를 사용합니다
    newPosition = position % super.getItemCount();
    } else {
    newPosition = position;
    }
    return super.getItem(newPosition);
    }
    ....
    }
  1. 액티비티(NativeCarouselActivity)에 CarouselAdapter(isInfiniteLoopEnabled)를 추가하세요.

    public class NativeCarouselActivity extends AppCompatActivity {
    ....
    // 무한 루프를 사용할지 설정
    private boolean isInfiniteLoopEnabled = true;
    ....
    @Override
    public void onCreate(final Bundle savedInstanceState) {
    ....
    adapter = new CarouselAdapter(isInfiniteLoopEnabled);
    ....
    }
    ....
    }

✏️  참고
무한 루프 등의 기능을 적용하여 광고 객체를 재사용하는 경우, 광고와 뷰의 연결을 해제해야 합니다. 자세한 내용은 광고 객체 재사용하기 토픽을 참고하세요.

첫번째 광고 앞에 마지막 광고를 부분적으로 노출하기

무한 루프 기능을 적용하는 경우, 첫번째 광고 아이템의 왼쪽에 마지막 광고 아이템을 부분적으로 노출하여 사용자가 좌우 양방향으로 스크롤할 수 있음을 알려줄 수 있습니다.

다음의 예시를 참고하여 광고 아이템 목록의 중간이 보여지도록 배열 크기 * 적당히 큰 수로 위치를 변경하세요.

public class NativeCarouselActivity extends AppCompatActivity {
....
private void onAdsLoaded(final Collection<NativeAd> nativeAds) {
....
final List<CarouselItem> items = buildCarouselItems(nativeAds);
adapter.submitList(items);
if (infiniteLoop) {
final RecyclerView.LayoutManager layoutManager = binding.carousel.getLayoutManager();
if (layoutManager != null) {
// item.size(): 첫번째 광고가 보이고, 그 앞에 마지막 아이템이 살짝 보이기 위해 이 값을 기준으로 합니다.
// 10000: 사용자가 앞으로 계속 스크롤을 하여도 무한루프인 것처럼 느낄 수 있도록 적당히 큰 수를 곱합니다.
layoutManager.scrollToPosition(items.size() * 10000);
}
}
....
}
....
}

광고 이벤트 리스너 등록하기

캐러셀에 포함된 광고가 사용자에게 노출되거나 사용자가 클릭하는 등의 광고 이벤트가 발생하는 시점에 특정 동작을 추가할 수 있습니다.

⚠️ 주의
광고 이벤트 리스너로 피드 진입 슬라이드의 노출 또는 클릭 이벤트를 수신할 수는 없습니다.

광고 이벤트 리스너를 등록하려면 다음의 절차를 따르세요.

  1. NativeAd 객체에 NativeAdEventListener를 추가하세요.

    public class NativeCarouselActivity extends AppCompatActivity {
    ....
    private NativeAdEventListener nativeAdEventListener = new NativeAdEventListener() {
    @Override
    public void onImpressed(final NativeAd nativeAd) {
    }

    @Override
    public void onClicked(final NativeAd nativeAd) {
    }

    @Override
    public void onRewardRequested(final NativeAd nativeAd) {
    }

    @Override
    public void onRewarded(
    final NativeAd nativeAd,
    final RewardResult nativeAdRewardResult
    ) {
    }

    @Override
    public void onParticipated(final NativeAd nativeAd) {
    }
    };
    ....
    }
  2. NativeAd에 이전 단계에서 등록한 리스너(NativeAdEventListener)를 등록하세요.

    ✏️  참고
    광고를 받은 직후 시점에 리스너를 등록하는 것을 권장합니다.

    public class NativeCarouselActivity extends AppCompatActivity {
    ....
    private void onAdsLoaded(final Collection<NativeAd> nativeAds) {
    ....
    for (final NativeAd nativeAd : nativeAds) {
    nativeAd.addNativeAdEventListener(nativeAdEventListener);
    }
    ....
    }
    }

광고 아이템 크기 줄이기

LinearLayoutManagercheckLayoutParams를 조정하여 슬라이드의 너비를 줄일 수 있습니다. 이를 통해 광고 아이템의 크기를 줄이고 앞뒤 광고를 부분적으로 노출해 사용자의 스크롤을 유도할 수 있습니다.

다음은 광고 아이템의 크기를 캐러셀(RecyclerView) 너비를 기준으로 80%만 차지하도록 줄이는 예시입니다.

public class NativeCarouselActivity extends AppCompatActivity {
....
// 화면 폭에 대한 Carousel 아이템의 폭 비율
private final float itemWidthPercentOfScreen = 0.8f;

// LinearLayoutManager를 사용하고 HORIZONTAL로 설정하여 수평으로 일렬로 정렬된 레이아웃을 사용합니다
private LinearLayoutManager layoutManager = new LinearLayoutManager(
this,
HORIZONTAL,
false
) {
// Carousel 아이템의 폭을 설정한 비율에 맞게 변경합니다.
@Override
public boolean checkLayoutParams(final RecyclerView.LayoutParams lp) {
lp.width = Math.round(getWidth() * itemWidthPercentOfScreen);
return true;
}
};
....
}

광고 아이템의 크기를 캐러셀(RecyclerView) 너비와 상관 없이 줄이려면 다음의 예시를 참고하세요.

public class NativeCarouselActivity extends AppCompatActivity {
....
// 화면 폭에 대한 Carousel 아이템의 폭 차이
private final int itemWidthDifferenceFromScreenDp = 76;

// LinearLayoutManager를 사용하고 HORIZONTAL로 설정하여 수평으로 일렬로 정렬된 레이아웃을 사용합니다
private final LinearLayoutManager layoutManager = new LinearLayoutManager(
this,
HORIZONTAL,
false
) {
// Carousel 아이템의 폭을 설정한 비율에 맞게 변경합니다
@Override
public boolean checkLayoutParams(final RecyclerView.LayoutParams lp) {
lp.width = getWidth() - dpToPx(itemWidthDifferenceFromScreenDp);
return true;
}
}
....
}

광고 아이템 사이의 여백 설정하기

RecyclerView에서 광고 아이템 사이의 여백을 설정하려면 다양한 방법을 적용할 수 있습니다. 이 가이드에서는 ItemDecoration을 사용하는 방법을 제시합니다. 다음의 절차를 따르세요.

  1. 다음 예시를 참고하여 RecylerView에서 사용할 ItemDecoration을 선언하세요.

    ✏️  참고
    샘플 앱에서는 PaddingDividerDecoration 코드를 참고하세요.

    /**
    * 아이템 사이의 간격을 넣기 위한 ItemDecoration
    */
    class PaddingDividerDecoration extends RecyclerView.ItemDecoration {
    private final int paddingDp;

    /**
    * @param paddingDp: 아이템 간의 간격, 단위는 dp
    */
    public PaddingDividerDecoration(final int paddingDp) {
    this.paddingDp = paddingDp;
    }

    @Override
    public void getItemOffsets(
    final Rect outRect,
    final View view,
    final RecyclerView parent,
    final RecyclerView.State state
    ) {
    // 여백을 한 쪽만 설정하면 PagerSnapHelper가 약간 어긋나게 동작하므로, 양쪽에 동일하게 여백을 넣습니다.
    outRect.left = dpToPx(paddingDp / 2);
    outRect.right = dpToPx(paddingDp / 2);
    }

    private int dpToPx(final int dp) {
    return (int) (dp * Resources.getSystem().getDisplayMetrics().density);
    }
    }
  2. 캐러셀을 초기화할 때 이전 단계에서 선언한 ItemDecoration을 추가하세요.

    class NativeCarouselActivity : AppCompatActivity() {
    // Carousel 아이템 간의 사이 간격
    private val itemPaddingDp = 16
    ....
    private fun initCarousel() {
    ....
    // 아이템 간에 여백을 넣는 Decoration 추가
    binding.carousel.addItemDecoration(PaddingDividerDecoration(itemPaddingDp))
    }
    ....
    }

앞뒤 광고 아이템을 부분적으로 노출하기

광고 아이템의 크기를 줄이거나 사이 여백을 설정한 후에 앞뒤 광고 아이템이 부분적으로 노출되던 레이아웃이 변경될 수 있습니다. 이로 인해 사용자가 캐러셀에서 광고 아이템을 스크롤 할 수 있음을 인식하지 못할 수도 있습니다. 이러한 문제를 해결하기 위해 앞뒤 광고 아이템을 부분적으로 노출하도록 설정할 수 있습니다.

앞뒤 광고 아이템을 부분적으로 노출하려면 RecyclerView에 android:clipToPaddingfalse로 설정한 후 paddingHorizontal을 추가하세요.

<!-- activity_native_carousel.xml -->
....
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/carousel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipToPadding="false"
android:paddingHorizontal="32dp" />
....