好的,以前公司就有在用穿山甲的广告平台,然后有些项目是 React native ,以前是有封装一个模块,但是中间不只包含穿山甲还有很多其他乱七八糟的模块,干脆边写博客边把头条穿山甲这个模块重写出来吧

首先我们先用 create-react-native-module 工具搭建一个项目吧

create-react-native-module --module-prefix hxf --package-identifier com.haxifang --generate-example oceanengine

我带了一些个性化的参数,create-react-native-module 具体的参数介绍可以看看我另外一篇博客:https://bin.zmide.com/?p=514

我这里就不过多介绍这些东西了,接下来我们可以打开头条穿山甲的广告对接文档,我这里就不把全部文档讲一遍了,相信大家都是有平台账号并且能看到的

手把手封装一个 React native Module 之实战封装穿山甲广告模块 android 部分之开屏广告-天真的小窝

头条穿山甲开屏广告大致对接原理的话就是:原生创建一个开屏广告的 Activity ,然后 react native 调用相关方法启动这个 Activity (这里我默认就是认为你有 android 开发基础的哈,也不去讲这些 android 开发基础了,当然没基础的话也可以跟着一步步对接,相关模块我也后续会放到 github ,希望各位小伙伴能给个 star ,我也会把相关对接文档补全的…)首先我们知道 react native 有两种模块形式:Native Modules 和 Native UI Components,

Native Modules 就是暴露一些相关的方法提供给 RN 去调用一些原生的特殊 api

Native UI Components 的话就是一个 UI 组件可以嵌入到布局中的

我们通过上面的对接分析可以知道开屏广告的话就不属于 UI 组件的范围,而是通过原生这边去跳转原生的 Activity 去达到一个开屏的效果。

属于 UI 组件的话大概是像 信息流广告,Banner 广告,Draw 信息流等等…这种类型的。以后我也会讲到怎么去写这一类的组件的…

好了前期的开发思路分析就到这里,我们看看刚刚生成的 hxf-oceanengine 模块,打开代码我们看看这个模块的开发目录结构

手把手封装一个 React native Module 之实战封装穿山甲广告模块 android 部分之开屏广告-天真的小窝

我们看到有 4 个文件夹,android( 这就是 android 的开发目录了 ),example(这是演示项目目录,可以用来调试我们的模块) ,ios(这是 ios 模块对应的开发目录),scripts(这是 srcipts 目录用来放 js 文件的吧,它里面写了一个事例和注释,有兴趣的小伙伴可以了解一下)

我这里就主要讲 android 模块的实现,所以 ios 文件夹我们就不去看了,还有一个就是 example 我们在里面调试模块的可用性吧

我先配置一下 example 演示项目吧,它和我们的 react native 项目是没有什么区别的 ,可以把它先跑起来看看

手把手封装一个 React native Module 之实战封装穿山甲广告模块 android 部分之开屏广告-天真的小窝
项目结构
手把手封装一个 React native Module 之实战封装穿山甲广告模块 android 部分之开屏广告-天真的小窝
跑起来后

启动项目并没有什么骚操作,正常的进入到 example 文件夹后 react-native run-android 就能跑起来了

事例项目的话引入的方式是通过 node module 方式,这样会使我们用 Android studio 修改调试 module 的代码会非常不方便,于是我们需要手动 link 一下 hxf-byted-ad 模块,手动 link 后我们会看到 example 的 android/settings.gradle 文件中多了两行代码

include ':hxf-byted-ad'
project(':hxf-byted-ad').projectDir = new File(rootProject.projectDir, '../node_modules/hxf-byted-ad')

需要把它指向我们的模块下面的 android 目录,改成如下代码

include ':hxf-byted-ad'
project(':hxf-byted-ad').projectDir = new File(rootProject.projectDir, '../../android')

接下来就可以用 Android studio 打开我们的 example 下面的 android 目录了,打开后大概能看到如下的目录结构

手把手封装一个 React native Module 之实战封装穿山甲广告模块 android 部分之开屏广告-天真的小窝

现在我们就可以通过 Android studio 随意的修改 Android hxf-byted-ad 的代码啦

前期工作差不多了,现在我们下载头条穿山甲提供给我们的 arr 格式的 SDK 模块包,在我们的 hxf-byted-ad 的 android 文件夹下创建一个 libs 文件夹,将在穿山甲下载的 arr 文件放到此目录下

手把手封装一个 React native Module 之实战封装穿山甲广告模块 android 部分之开屏广告-天真的小窝

打开 build.gradle 文件,添加引入

....

dependencies {
    ....
    implementation fileTree(dir: 'libs', include: ['*.aar'])
    ....
}

....

我还是贴一张图吧,我这里还引用了 com.github.bumptech.glide 模块用来处理图片资源的,如果你没有用到的话是可以不需要引入的

手把手封装一个 React native Module 之实战封装穿山甲广告模块 android 部分之开屏广告-天真的小窝

接下来就是广告平台要求的权限和一些需要的配置,编辑我们的 AndroidManifest.xml 文件,我这里直接贴我配置的了,详细的配置可以看文档

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="com.haxifang">

    <!--必要权限-->
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

    <!--可选权限-->
    <uses-permission android:name="android.permission.READ_PHONE_STATE" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
    <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>
    <uses-permission android:name="android.permission.GET_TASKS"/>
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
    <uses-permission android:name="android.permission.WAKE_LOCK" />

    <application>

        <activity android:name=".ttad.modules.SplashActivity" />

        <provider
            android:name="com.bytedance.sdk.openadsdk.TTFileProvider"
            android:authorities="${applicationId}.TTFileProvider"
            android:exported="false"
            android:grantUriPermissions="true">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/file_paths" />
        </provider>
        <provider
            android:name="com.bytedance.sdk.openadsdk.multipro.TTMultiProvider"
            android:authorities="${applicationId}.TTMultiProvider"
            android:exported="false" />
    </application>


</manifest>

这里有用到一个 @xml/file_paths 文件,我们需要在 res 文件夹中创建一个 xml 的目录然后添加一个 file_paths.xml 文件,代码如下

<?xml version="1.0" encoding="utf-8"?>
<paths>
    <external-path name="tt_external_root" path="." />
    <external-path name="tt_external_download" path="Download" />
    <external-files-path name="tt_external_files_download" path="Download" />
    <files-path name="tt_internal_file_download" path="Download" />
    <cache-path name="tt_internal_cache_download" path="Download" />
</paths>

手把手封装一个 React native Module 之实战封装穿山甲广告模块 android 部分之开屏广告-天真的小窝

创建好配置文件后我们可以看到 java 文件夹中已经有两个系统帮我们创建好的类

BytedAdModule,是一个 Native Modules 包的实现类

BytedAdPackage,我们写好的 Native Modules 或者 Native UI Components 都是要通过这个类来注册到 react native 中去的

好的,我们简单了解了一下这两个类的用途后先不管它两,我们先创建一个 SplashActivity 和 tt_splash_activity.xml 用来写我们开屏广告的代码

手把手封装一个 React native Module 之实战封装穿山甲广告模块 android 部分之开屏广告-天真的小窝

这里我先贴上我实现的代码,就是很简单的实现一个开屏广告,你也可以直接把穿山甲提供的 Demo 中的代码用起来

package com.haxifang.ttad.modules;

import android.app.Activity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.FrameLayout;

import androidx.annotation.MainThread;
import androidx.annotation.Nullable;

import com.bytedance.sdk.openadsdk.AdSlot;
import com.bytedance.sdk.openadsdk.TTAdConfig;
import com.bytedance.sdk.openadsdk.TTAdConstant;
import com.bytedance.sdk.openadsdk.TTAdNative;
import com.bytedance.sdk.openadsdk.TTAdSdk;
import com.bytedance.sdk.openadsdk.TTSplashAd;
import com.haxifang.R;

public class SplashActivity extends Activity {

    static String TAG = "头条开屏广告";
    static String ttAppId;

    private TTAdNative mTTAdNative;
    private FrameLayout mSplashContainer;
    // 是否强制跳转到主页面
    private boolean mForceGoMain;
    
    // 开屏广告加载超时时间,建议大于1000,这里为了冷启动第一次加载到广告并且展示,示例设置了2000ms
    private static final int AD_TIME_OUT = 2000;
    private static final int MSG_GO_MAIN = 1;

    // 开屏广告是否已经加载
    private boolean mHasLoaded;

    private String code_id;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.tt_splash_activity);

        // 读取 code id
        Bundle extras = getIntent().getExtras();
        ttAppId = extras.getString("appid");
        code_id = extras.getString("codeid");
        
        // 初始化广告 SDK
        initAD();
        
        // 在合适的时机申请权限,如read_phone_state,防止获取不了 imei 时候,下载类广告没有填充的问题
        // 在开屏时候申请不太合适,因为该页面倒计时结束或者请求超时会跳转,在该页面申请权限,体验不好
        // TTAdManagerHolder.getInstance(this).requestPermissionIfNecessary(this);
        
        // 初始化自定义广告 View
        initView();

        // 加载开屏广告
        loadSplashAd();
    }

    private void initAD() {
        
        TTAdSdk.init(this, new TTAdConfig.Builder()
                .appId(ttAppId)
                .useTextureView(true) //使用TextureView控件播放视频,默认为SurfaceView,当有SurfaceView冲突的场景,可以使用TextureView
                .appName("答妹")
                .titleBarTheme(TTAdConstant.TITLE_BAR_THEME_DARK)
                .allowShowNotify(true) //是否允许sdk展示通知栏提示
                .allowShowPageWhenScreenLock(true) //是否在锁屏场景支持展示广告落地页
                .debug(true) //测试阶段打开,可以通过日志排查问题,上线时去除该调用
                .directDownloadNetworkType(TTAdConstant.NETWORK_STATE_WIFI, TTAdConstant.NETWORK_STATE_3G) //允许直接下载的网络状态集合
                .supportMultiProcess(false)
                .build());

        mTTAdNative = TTAdSdk.getAdManager().createAdNative(this);
    }
    
    // 初始化开屏广告 View
    private void initView() {
        // 初始化广告渲染组件
        mSplashContainer = this.findViewById(R.id.splash_container);
    }

    // 加载开屏广告方法
    private void loadSplashAd() {

        // 创建开屏广告请求参数 AdSlot ,具体参数含义参考文档
        AdSlot adSlot = new AdSlot.Builder()
                .setCodeId(code_id)
                .setSupportDeepLink(true)
                .setImageAcceptedSize(1080, 1920)
                .build();

        // 请求广告,调用开屏广告异步请求接口,对请求回调的广告作渲染处理
        mTTAdNative.loadSplashAd(adSlot, new TTAdNative.SplashAdListener() {
            @Override
            @MainThread
            public void onError(int code, String message) {
                // 广告渲染失败
                Log.d(TAG, message);
                mHasLoaded = true;
                showToast(message + " - " + code_id);

                // 关闭开屏广告
                goToMainActivity();
            }

            @Override
            @MainThread
            public void onTimeout() {
                // 开屏广告渲染超时
                mHasLoaded = true;
                showToast("加载超时");

                // 关闭开屏广告
                goToMainActivity();
            }

            @Override
            @MainThread
            public void onSplashAdLoad(TTSplashAd ad) {
                Log.d(TAG, "开屏广告请求成功");
                mHasLoaded = true;
                
                if (ad == null) {
                    // 未知错误获取到的广告对象为空,关闭广告
                    goToMainActivity();
                    return;
                }

                // 获取SplashView
                View view = ad.getSplashView();
                mSplashContainer.removeAllViews();

                // 把SplashView 添加到ViewGroup中,注意开屏广告view:width >=70%屏幕宽;height >=50%屏幕宽
                mSplashContainer.addView(view);

                // 设置不开启开屏广告倒计时功能以及不显示跳过按钮,如果这么设置,您需要自定义倒计时逻辑
                // ad.setNotAllowSdkCountdown();

                // 设置SplashView的交互监听器
                ad.setSplashInteractionListener(new TTSplashAd.AdInteractionListener() {
                    @Override
                    public void onAdClicked(View view, int type) {
                        Log.d(TAG, "onAdClicked");
                        // showToast("开屏广告点击");
                    }

                    @Override
                    public void onAdShow(View view, int type) {
                        Log.d(TAG, "onAdShow");
                        // showToast("开屏广告展示");
                    }

                    @Override
                    public void onAdSkip() {
                        // returnIntent.putExtra("onAdSkip", true);
                        Log.d(TAG, "onAdSkip");
                        // showToast("开屏广告跳过");
                        goToMainActivity();

                    }

                    @Override
                    public void onAdTimeOver() {
                        Log.d(TAG, "onAdTimeOver");
                        // showToast("开屏广告倒计时结束");
                        goToMainActivity();
                    }
                });
            }
        }, AD_TIME_OUT);
    }

    // 关闭开屏广告方法
    private void goToMainActivity() {
        if(mSplashContainer != null) {
            mSplashContainer.removeAllViews();
        }
        this.overridePendingTransition(0, 0); // 不要过渡动画
        this.finish();
    }

    private void showToast(String msg) {
        // TToast.show(this, "splash:" + msg);
    }

}

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:background="#FFFFFF">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="0.8"
        android:orientation="vertical"
        >
        <FrameLayout
            android:id="@+id/splash_container"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:background="#FFFFFF"
            >
        </FrameLayout>
    </LinearLayout>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="0.2"
        android:gravity="center"
        android:orientation="vertical"
        >
        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:padding="10dp"
            android:gravity="center"
            android:textColor="#AAA"
            android:text="正在启动中…"/>
    </LinearLayout>

</LinearLayout>

开屏实现好后,我们把这个模块暴露给 RN 吧,回到我们的 BytedAdModule 类中,就是我们上面看的系统帮我们创建的,改成如下代码我们将暴露一个 loadSplashAd 方法给 RN 该方法接收两个参数 appid 和 codeid(这个可以到穿山甲平台申请),我们给这个模块命名为 BytedADSplash

package com.haxifang;

import android.app.Activity;
import android.content.Intent;

import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.Callback;
import com.haxifang.ttad.TTAdManagerHolder;
import com.haxifang.ttad.modules.SplashActivity;

public class BytedAdModule extends ReactContextBaseJavaModule {

    private final ReactApplicationContext reactContext;

    public BytedAdModule(ReactApplicationContext reactContext) {
        super(reactContext);
        this.reactContext = reactContext;
    }

    @Override
    public String getName() {
        return "BytedADSplash";
    }

    @ReactMethod
    public void loadSplashAd(String appid, String codeid) {

        Intent intent = new Intent(reactContext, SplashActivity.class);
        try {
            intent.putExtra("codeid", codeid);
            intent.putExtra("appid", codeid);
            final Activity context = getCurrentActivity();
            context.overridePendingTransition(0, 0); // 不要过渡动画
            context.startActivityForResult(intent, 10000);
        } catch (Exception e) {
            e.printStackTrace();
        }

    }
}

然后进入到 BytedAdPackage 类的 createNativeModules 方法中注册这个模块

package com.haxifang;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ViewManager;

public class BytedAdPackage implements ReactPackage {
    @Override
    public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
        List<NativeModule> modules =  new ArrayList<>();
        modules.add(new BytedAdModule(reactContext));
        return modules;
    }

    @Override
    public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
        return Collections.emptyList();
    }
}

最后我们回到模块的 index.js 在项目的根目录下,导出我们的启动开屏广告模块

import { NativeModules } from "react-native";

const { BytedADSplash } = NativeModules;

export const loadSplashAd = (appid, codeid) => {
  BytedADSplash.loadSplashAd(appid, codeid);
};

export default { loadSplashAd };

然后到 example 项目下执行 rm -rf node_modules/ && yarn && react-native run-android 

在 example 中记得导入和使用 loadSplashAd 来启动开屏广告哦,最后别忘记传入 appid 和 codeid (开屏广告位id)…

import {loadSplashAd} from 'hxf-byted-ad';

....

// 在你想要启动开屏广告的地方调用
loadSplashAd(appid, codeid);

....

好啦,我们下篇博客见鸭,有问题随时可以给我留言回复哦,我看到一定会回复的