pangle_flutter

Introduction: Flutter 版穿山甲 SDK,支持 Android、iOS。A Flutter plugin that supports Pangle SDK on Android and iOS.
More: Author   ReportBugs   
Tags:

pub package Licence flutter

简介

pangle_flutter是一款集成了穿山甲 Android 和 iOS SDK 的 Flutter 插件。部分代码由官方范例修改而来。

官方文档(需要登陆)

使用文档

集成步骤

1. 添加 yaml 依赖

dependencies:
  # 添加依赖
  pangle_flutter: latest

2. Android 和 iOS 额外配置

  • iOS 从版本 3.4.1.9 开始 pod 方式变更

    原话:【变更】pod 方式变更,国内使用 pod 'Ads-CN',海外使用 pod 'Ads-Global'

    本项目默认集成Ads-CN, 如果你是国内 APP,无需额外配置;如果你是海外 APP,请参照如下配置:

    打开你 flutter 应用 ios 项目下的Podfile,在target 'Runner do上面添加如下代码即可(如果不熟悉 Podfile,也可以参考本项目example/ios/Podfile里面的配置)。

    # add code begin
    def flutter_install_ios_plugin_pods(ios_application_path = nil)
      # defined_in_file is set by CocoaPods and is a Pathname to the Podfile.
      ios_application_path ||= File.dirname(defined_in_file.realpath) if self.respond_to?(:defined_in_file)
      raise 'Could not find iOS application path' unless ios_application_path
    
      # Prepare symlinks folder. We use symlinks to avoid having Podfile.lock
      # referring to absolute paths on developers' machines.
    
      symlink_dir = File.expand_path('.symlinks', ios_application_path)
      system('rm', '-rf', symlink_dir) # Avoid the complication of dependencies like FileUtils.
    
      symlink_plugins_dir = File.expand_path('plugins', symlink_dir)
      system('mkdir', '-p', symlink_plugins_dir)
    
      plugins_file = File.join(ios_application_path, '..', '.flutter-plugins-dependencies')
      plugin_pods = flutter_parse_plugins_file(plugins_file)
      plugin_pods.each do |plugin_hash|
        plugin_name = plugin_hash['name']
        plugin_path = plugin_hash['path']
        if (plugin_name && plugin_path)
          symlink = File.join(symlink_plugins_dir, plugin_name)
          File.symlink(plugin_path, symlink)
    
          pod plugin_name, :path => File.join('.symlinks', 'plugins', plugin_name, 'ios')
          if plugin_name == 'pangle_flutter'
            # cn 代表国内,global 代表海外
            pod 'pangle_flutter/global', :path => File.join('.symlinks', 'plugins', plugin_name, 'ios')
          end
    
        end
      end
    end
    # add code end
    

使用说明

目前 iOS 类信息流广告和横幅广告还处于预览版,部分功能存在不能正常使用的情况(如点击事件传递问题,渲染慢),不建议用于正式环境

1. 信息流广告

  1. 原生自渲染信息流广告: 指定图片大小或比例固定,由开发者根据 imageMode 自行渲染。 模板渲染广告:指定整个广告宽高,由 SDK 自动适配传入的宽高进行渲染。
  2. 原生自渲染信息流广告本模块暂不能整个 item 自定义宽高,只能使用PangleImgSize中的值指定图片宽高比例,模版渲染广告可指定期望宽高,但必须跟广告后台说明的宽高对应。
  3. 目前根据 SDK Demo 所知,模板类广告每次只能传入一种模板宽高,并且渲染广告时获取不到该广告所使用的模板类型。因此如果选择多种模板,可能导致渲染出来的效果不佳。

pangle_flutter

pangle_flutter

2. iOS 使用纯 OC 开发的项目导入该模块

  1. 创建一个 Swift 文件,名称随意
  2. 根据提示选择Create Bridging Header。如果没有提示,请自行搜索如何创建。

OC 导入 Swift 模块

使用步骤

1. 初始化

import 'package:pangle_flutter/pangle_flutter.dart';
/// 如果在 runApp 方法调用之前初始化,加入下面这句代码
WidgetsFlutterBinding.ensureInitialized();
/// 初始化,未列出所有参数
/// [kAppId] 申请穿山甲广告位后得到的 appID
await pangle.init(
  iOS: IOSConfig(appId: kAppId),
  android: AndroidConfig(appId: kAppId),
);

2. 开屏广告

/// 全屏类型
/// [kSplashId] 开屏广告 ID, 对应 Android 的 CodeId,对应 iOS 的 slotID
await pangle.loadSplashAd(
  iOS: IOSSplashConfig(slotId: kSplashId, isExpress: false),
  android: AndroidSplashConfig(slotId: kSplashId, isExpress: false),
);


/// 自定义类型
/// 同 Widget 类用法
SplashView(
  iOS: IOSSplashConfig(slotId: kSplashId, isExpress: false),
  android: AndroidSplashConfig(slotId: kSplashId, isExpress: false),
  backgroundColor: Colors.white,
  /// 广告展示
  onShow: (){},
  /// 广告获取失败
  onError: (){},
  /// 广告被点击
  onClick: (){},
  /// 广告被点击跳过
  onSkip: (){},
  /// 广告倒计时结束
  onTimeOver: (){},
);

3. 激励视频广告

/// [kRewardedVideoId] 激励视频广告 ID, 对应 Android 的 CodeId,对应 iOS 的 slotID
pangle.loadRewardVideoAd(
   iOS: IOSRewardedVideoConfig(slotId: kRewardedVideoId),
   android: AndroidRewardedVideoConfig(slotId: kRewardedVideoId),
 );

4. Banner 广告

/// Banner 通过 PlatformView 实现,使用方法同 Widget
/// [kBannerId] Banner 广告 ID, 对应 Android 的 CodeId,对应 iOS 的 slotID
BannerView(
  iOS: IOSBannerAdConfig(slotId: kBannerId),
  android: AndroidBannerAdConfig(slotId: kBannerId),
),
  • 切换可点击状态
// 因 iOS 的 EXPRESS 类型的广告内部使用 WebView 渲染,而 WebView 与 FlutterView 存在部分点击事件冲突,故提供该解决方案
final _bannerKey = GlobalKey<BannerViewState>();
// 外部控制该广告位是否可点击
_bannerKey.currentState.setUserInteractionEnabled(enable);

5. 信息流广告

  • 获取信息流数据
/// 信息流实现逻辑
/// 首先进行网络请求,得到信息流数据
///
/// PangleFeedAd 相应字段: 
/// [code] 响应码,0 成功,-1 失败
/// [message] 错误时,调试信息
/// [count] 获得信息流数量,一般同上面传入的 count,最终结果以此 count 为主
/// [data] (string list) 用于展示信息流广告的键 id
 PangleFeedAd feedAd = await pangle.loadFeedAd(
   iOS: IOSFeedAdConfig(slotId: kFeedId, count: 2),
   android: AndroidFeedAdConfig(slotId: kFeedId, count: 2),
 );
  • 加载数据
/// 使用方法
/// 你的数据模型
class Item {
  /// 添加字段
  final String feedId;
}
final items = <Item>[];
final feedAdDatas = feedAd.data;
final items = Item(feedId: feedAdDatas[0]);
items.insert(Random().nextInt(items.length), item);
/// Widget 使用
FeedView(
  id: item.feedId,
  onRemove: () {
    setState(() {
      items.removeAt(index);
    });
  },
)
  • 切换是否可点击状态
// 因 iOS 的 EXPRESS 类型的广告内部使用 WebView 渲染,而 WebView 与 FlutterView 存在部分点击事件冲突,故提供该解决方案
// 1. 继承 GlobalObjectKey 实现自己的 key
class _ItemKey extends GlobalObjectKey<FeedViewState> {
  _ItemKey(Object value) : super(value);
}
// 2. 为 FeedView 提供自己的 key
FeedView(
  key: _ItemKey(item.feedId),
  ...
)
// 3. 为需要计算位置的 Widget 提供 key, 如
final _titleKey = GlobalKey();
AppBar(key: _titleKey)
final _naviKey = GlobalKey();
BottomNavigationBar(key: _naviKey)
// 4. 为 FeedView 容器提供 ScrollController, 如
final _controller = ScrollController();
ListView(controller: _controller)
// 5. 监听 controller 滚动事件,并动态切换可点击状态
@override
void initState() {
  super.initState();
  _loadFeedAd();
  _controller.addListener(_onScroll);
}

@override
void dispose() {
  _controller.removeListener(_onScroll);
  super.dispose();
}
_onScroll() {
  if (!Platform.isIOS) {
    return;
  }

  RenderBox titleBox = _titleKey.currentContext.findRenderObject();
  var titleSize = titleBox.size;
  var titleOffset = titleBox.localToGlobal(Offset.zero);

  final minAvailableHeigt = titleOffset.dy + titleSize.height;

  RenderBox naviBox = _naviKey.currentContext.findRenderObject();
  var naviOffset = naviBox.localToGlobal(Offset.zero);

  final maxAvailableHeight = naviOffset.dy;

  /// 检测各个 item 的宽高、偏移量是否满足点击需求
  for (var value in feedIds) {
    _switchUserInteraction(maxAvailableHeight, minAvailableHeigt, value);
  }
}

void _switchUserInteraction(
  double maxAvailableHeight,
  double minAvailableHeigt,
  String id,
) {
  var itemKey = _ItemKey(id);
  RenderBox renderBox = itemKey.currentContext.findRenderObject();
  var size = renderBox.size;
  var offset = renderBox.localToGlobal(Offset.zero);

  /// 最底部坐标不低于 NavigationBar, 最顶部不高于 AppBar
  var available = offset.dy + size.height < maxAvailableHeight &&
    offset.dy > minAvailableHeigt;
  itemKey.currentState.setUserInteractionEnabled(available);
}

6. 插屏广告

 final result = await pangle.loadInterstitialAd(
   iOS: IOSInterstitialAdConfig(
     slotId: kInterstitialId,
     isExpress: true,

     /// 该宽高为你申请的广告位宽高,请根据实际情况赋值
     imgSize: PangleImgSize.interstitial600_400,
   ),
   android: AndroidInterstitialAdConfig(
     slotId: kInterstitialId,
   ),
 );
print(jsonEncode(result));

开发说明

  1. 开屏广告放在 runApp 之前调用体验最佳
  2. iOS 信息流广告的点击事件需要传入rootViewController,使用的是(UIApplication.shared.delegate?.window??.rootViewController)!,暂未发现问题。

  3. BannerViewFeedView通过PlatformView实现。在安卓上,PlatformView最低支持 API 20。

贡献

  • 有任何更好的实现方式或增加额外的功能,欢迎提交 PR。
  • 有任何使用上的问题,欢迎提交 issue。

交流

因微信群 7 日有效期,就不放微信群二维码了。备注项目名加我好友,加入微信交流群。

备注:pangle_flutter

Flutter 开发交流

Apps
About Me
GitHub: Trinea
Facebook: Dev Tools