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

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

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

实现在RecyclerView中使用原生广告

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

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

通过RecyclerView.Adapter的getItemViewType方法来区分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

相关推荐
千里马学框架4 天前
一起学 Android 14:ShellTransition 屏幕旋转过程深度剖析
android·智能手机·性能优化·framework·性能·屏幕旋转·rotation
美狐美颜SDK开放平台4 天前
开发直播APP时如何接入视频美颜SDK?开发流程与注意事项
android·人工智能·计算机视觉·音视频·直播美颜sdk
AFinalStone4 天前
Android7 SystemUI源码解析(七)Keyguard锁屏模块深度解析
android·systemui
致远ccc4 天前
Google Play 上架前如何测试 App?多国家 Android 环境测试
android·app测试·googleplay·多国家应用测试
ttyyttemo4 天前
Kotlin 协程中的 Job 结构化并发与取消
android
sun0077004 天前
tbox 4g/5g切换,导致wan ip 改变,导致车机旧网络不可用。需要重启车机才行
android
其实防守也摸鱼4 天前
内网穿透与反向代理:原理、工具与实战指南
android·大数据·运维·安全·网络安全·自动化·渗透
AFinalStone4 天前
Android7 SystemUI 源码解析(四)NavigationBar 导航栏与 SystemBars
android·systemui
JMchen4 天前
属性动画原理与高级动画实现
android·kotlin·canvas
AFinalStone4 天前
Android7 SystemUI 源码解析(二)启动流程深度解析
android·systemui