Android AdMob(四)— 在RecyclerView中使用原生广告

现在许多App的主页都是以列表的形式显示数据,同时会在其中某些位置插入原生广告。本文简单介绍如何在RecyclerView中使用原生广告。

前置条件以及如何测试请移步Android Admob接入,本文不多赘述。

实现在RecyclerView中使用原生广告

虽然广告可以增加收入,但是肯定不能影响用户体验。进入页面时应当先显示正常的数据,广告在加载完成之后再显示即可。

在RecyclerView中同时显示正常的布局和广告布局

通过RecyclerView.AdaptergetItemViewType方法来区分Item项的类型,根据类型创建不同的布局,示例代码如下:

  • 实体类
kotlin 复制代码
const val LAYOUT_TYPE_NORMAL = 0
const val LAYOUT_TYPE_AD = 1

data class ExampleEntity(
    val layoutType: Int,
    val exampleContent: String
) {

    override fun toString(): String {
        return "ExampleEntity(layoutType=$layoutType, exampleContent='$exampleContent')"
    }
}
  • 正常Item的布局文件
ini 复制代码
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="50dp">

    <androidx.appcompat.widget.AppCompatTextView
        android:id="@+id/tv_content"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center_vertical"
        android:paddingStart="20dp"
        android:paddingEnd="0dp"
        tools:text="this is content example" />

    <View
        android:layout_width="match_parent"
        android:layout_height="0.5dp"
        android:background="@color/color_c2c7cc"
        app:layout_constraintBottom_toBottomOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
  • 广告Item的布局文件
ini 复制代码
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/fl_native_ad_container"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:paddingStart="16dp"
    android:paddingEnd="16dp" />
  • 适配器
kotlin 复制代码
class NativeAdExampleAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {

    private val containerData = ArrayList<ExampleEntity>()

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
        // 根据viewType创建不同类型的ViewHolder
        return if (viewType == LAYOUT_TYPE_AD) {
            AdvertiseItemViewHolder(LayoutAdvertiseItemBinding.inflate(LayoutInflater.from(parent.context), parent, false))
        } else {
            NormalItemViewHolder(LayoutTextContentItemBinding.inflate(LayoutInflater.from(parent.context), parent, false))
        }
    }

    override fun getItemCount(): Int {
        return containerData.size
    }

    override fun getItemViewType(position: Int): Int {
        return containerData[position].layoutType
    }

    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        when (holder) {
            is NormalItemViewHolder -> holder.itemViewBiding.tvContent.text = containerData[position].exampleContent

            is AdvertiseItemViewHolder -> {
                val nativeAdView = (holder.itemView.context as? NativeAdInListExampleActivity)?.getNativeAdView(position)
                if (nativeAdView != null) {
                    holder.itemViewBinding.flNativeAdContainer.removeAllViews()
                    holder.itemViewBinding.flNativeAdContainer.addView(nativeAdView)
                    holder.itemViewBinding.flNativeAdContainer.visibility = View.VISIBLE
                } else {
                    holder.itemViewBinding.flNativeAdContainer.visibility = View.GONE
                    holder.itemViewBinding.flNativeAdContainer.removeAllViews()
                }
            }
        }
    }

    fun setNewData(newData: List<ExampleEntity>?) {
        val currentItemCount = itemCount
        if (currentItemCount != 0) {
            containerData.clear()
            notifyItemRangeRemoved(0, currentItemCount)
        }
        if (!newData.isNullOrEmpty()) {
            containerData.addAll(newData)
            notifyItemRangeChanged(0, itemCount)
        }
    }

    fun release() {
        containerData.clear()
    }

    class AdvertiseItemViewHolder(val itemViewBinding: LayoutAdvertiseItemBinding) : RecyclerView.ViewHolder(itemViewBinding.root)

    class NormalItemViewHolder(val itemViewBiding: LayoutTextContentItemBinding) : RecyclerView.ViewHolder(itemViewBiding.root)
}

加载并显示原生广告

通过循环来创建正常元素和广告元素的集合,然后根据广告元素的数量加载原生广告并添加到页面中,示例代码如下:

kotlin 复制代码
class NativeAdInListExampleActivity : AppCompatActivity() {

    private lateinit var layoutNativeAdInListExampleActivityBinding: LayoutNativeAdInListExampleActivityBinding

    private val exampleDataList = ArrayList<ExampleEntity>()

    private val nativeAdLoader by lazy {
        // 使用广告测试广告位id
        AdLoader.Builder(this, "ca-app-pub-3940256099942544/2247696110")
            .forNativeAd { nativeAd ->
                // 如果在页面销毁后触发此回调,需要销毁NativeAd避免内存泄漏
                if (isDestroyed || isFinishing || isChangingConfigurations) {
                    nativeAd.destroy()
                    return@forNativeAd
                }
                populateNativeAdView(nativeAd)
            }
            .withNativeAdOptions(NativeAdOptions.Builder()
                // 设置视频是否静音播放
                .setVideoOptions(VideoOptions.Builder().setStartMuted(false).build())
                .build())
            .withAdListener(object : AdListener() {
                override fun onAdLoaded() {
                    super.onAdLoaded()
                    // 广告加载成功
                    nativeAdIndex.getOrNull(nativeAdViewList.lastIndex)?.let {
                        lifecycleScope.launch(Dispatchers.Main) {
                            nativeAdExampleAdapter.notifyItemChanged(it)
                        }
                    }
                    // 需要的原生广告数量小于广告item数量,继续加载广告
                    if (nativeAdViewList.size < nativeAdIndex.size) {
                        loadNativeAd()
                    }
                }

                override fun onAdFailedToLoad(loadAdError: LoadAdError) {
                    super.onAdFailedToLoad(loadAdError)
                    // 广告加载失败
                }

                override fun onAdOpened() {
                    super.onAdOpened()
                    // 广告页打开
                }

                override fun onAdClicked() {
                    super.onAdClicked()
                    // 广告被点击
                }

                override fun onAdClosed() {
                    super.onAdClosed()
                    // 广告页关闭
                }
            })
            .build()
    }

    private val nativeAdIndex = ArrayList<Int>()

    private val nativeAdList = ArrayList<NativeAd>()
    private val nativeAdViewList = ArrayList<NativeAdView>()

    private val nativeAdExampleAdapter = NativeAdExampleAdapter()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        layoutNativeAdInListExampleActivityBinding = LayoutNativeAdInListExampleActivityBinding.inflate(layoutInflater).also {
            setContentView(it.root)
            it.rvExampleDataContainer.adapter = nativeAdExampleAdapter
        }

        for (index in 1..30) {
            val layoutType: Int
            val content: String
            if (index % 4 == 0) {
                layoutType = LAYOUT_TYPE_AD
                content = ""
                // 记录广告元素在整体集合中的位置
                // 广告加载完成后根据此进行刷新,可以减少不必要的布局重绘。
                nativeAdIndex.add(index - 1)
            } else {
                layoutType = LAYOUT_TYPE_NORMAL
                content = "example data $index"
            }
            exampleDataList.add(ExampleEntity(layoutType, content))
        }

        nativeAdExampleAdapter.setNewData(exampleDataList)

        // 初始化SDK并加载原生广告
        MobileAds.initialize(this) { loadNativeAd() }
    }

    private fun loadNativeAd() {
        lifecycleScope.launch(Dispatchers.IO) { nativeAdLoader.loadAd(AdRequest.Builder().build()) }
    }

    @SuppressLint("InflateParams")
    private fun populateNativeAdView(nativeAd: NativeAd) {
        (LayoutInflater.from(this).inflate(R.layout.layout_admob_native_ad, null) as? NativeAdView)?.run {
            iconView = findViewById<AppCompatImageView>(R.id.iv_ad_app_icon).apply {
                nativeAd.icon?.let { setImageDrawable(it.drawable) }
                visibility = if (nativeAd.icon != null) View.VISIBLE else View.GONE
            }
            headlineView = findViewById<AppCompatTextView>(R.id.tv_ad_headline).apply {
                text = nativeAd.headline
            }
            advertiserView = findViewById<AppCompatTextView>(R.id.tv_advertiser).apply {
                text = nativeAd.advertiser
                visibility = if (nativeAd.advertiser != null) View.VISIBLE else View.INVISIBLE
            }
            starRatingView = findViewById<AppCompatRatingBar>(R.id.rb_ad_stars).apply {
                nativeAd.starRating?.let { rating = it.toFloat() }
                visibility = if (nativeAd.starRating != null) View.VISIBLE else View.INVISIBLE
            }
            bodyView = findViewById<AppCompatTextView>(R.id.tv_ad_body).apply {
                text = nativeAd.body
                visibility = if (nativeAd.body != null) View.VISIBLE else View.INVISIBLE
            }
            mediaView = findViewById<MediaView>(R.id.mv_ad_media).apply {
                nativeAd.mediaContent?.let {
                    mediaContent = it
                    it.videoController.videoLifecycleCallbacks = object : VideoController.VideoLifecycleCallbacks() {
                        override fun onVideoStart() {
                            super.onVideoStart()
                            // 视频开始
                        }

                        override fun onVideoEnd() {
                            super.onVideoEnd()
                            // 视频结束,结束后可以刷新广告
                        }

                        override fun onVideoPlay() {
                            super.onVideoPlay()
                            // 视频播放
                        }

                        override fun onVideoPause() {
                            super.onVideoPause()
                            // 视频暂停
                        }

                        override fun onVideoMute(mute: Boolean) {
                            super.onVideoMute(mute)
                            // 视频是否静音
                            // mute true 静音 false 非静音
                        }
                    }
                }
            }
            callToActionView = findViewById<AppCompatButton>(R.id.btn_ad_call_to_action).apply {
                text = nativeAd.callToAction
                visibility = if (nativeAd.callToAction != null) View.VISIBLE else View.INVISIBLE
            }
            priceView = findViewById<AppCompatTextView>(R.id.tv_ad_price).apply {
                text = nativeAd.price
                visibility = if (nativeAd.price != null) View.VISIBLE else View.INVISIBLE
            }
            storeView = findViewById<AppCompatTextView>(R.id.tv_ad_store).apply {
                text = nativeAd.store
                visibility = if (nativeAd.store != null) View.VISIBLE else View.INVISIBLE
            }
            layoutParams = FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.WRAP_CONTENT).apply {
                gravity = Gravity.BOTTOM
            }
            setNativeAd(nativeAd)
            nativeAdList.add(nativeAd)
            nativeAdViewList.add(this)
        }
    }

    fun getNativeAdView(layoutIndex: Int): NativeAdView? {
        return nativeAdViewList.getOrNull(nativeAdIndex.indexOf(layoutIndex))?.also { removeInParent(it) }
    }

    private fun removeInParent(view: View) {
        try {
            (view.parent as? ViewGroup)?.removeView(view)
        } catch (e: Exception) {
            e.printStackTrace()
        }
    }

    private fun releaseNativeAd() {
        nativeAdIndex.clear()
        for (nativeAd in nativeAdList) {
            nativeAd.destroy()
        }
        nativeAdList.clear()
        for (nativeAdView in nativeAdViewList) {
            removeInParent(nativeAdView)
            nativeAdView.destroy()
        }
        nativeAdViewList.clear()
    }

    override fun onDestroy() {
        super.onDestroy()
        exampleDataList.clear()
        nativeAdExampleAdapter.release()
        releaseNativeAd()
    }
}

效果演示与示例代码

最终效果如下图:

演示代码已在示例Demo中添加。

ExampleDemo github

ExampleDemo gitee

相关推荐
dal118网工任子仪9 分钟前
94,【2】buuctf web [安洵杯 2019]easy_serialize_php
android·前端·php
Kevin Coding3 小时前
Flutter使用Flavor实现切换环境和多渠道打包
android·flutter·ios
yashunan3 小时前
Web_php_unserialize
android·前端·php
taopi20245 小时前
android java系统弹窗的基础模板
android·java·开发语言
志尊宝5 小时前
深入探索 Android 技术:从基础到前沿
android
字节全栈_BjO6 小时前
mysql死锁排查_mysql 死锁问题排查
android·数据库·mysql
恋猫de小郭20 小时前
Android Studio 正式版 10 周年回顾,承载 Androider 的峥嵘十年
android·ide·android studio
aaaweiaaaaaa1 天前
php的使用及 phpstorm环境部署
android·web安全·网络安全·php·storm
工程师老罗1 天前
Android记事本App设计开发项目实战教程2025最新版Android Studio
android
pengyu1 天前
系统化掌握 Dart 编程之异常处理(二):从防御到艺术的进阶之路
android·flutter·dart