开发首屏广告(Android)简述
作为一个成熟的应用, 必须要有广告. 那么, 如何优雅地开发广告呢? 需要注意一些细节.
本文提供一个简单的示例, 代码仅供参考.
需求:
具体来说, 就是
1. 显示本地存储广告图片, 点击图片, 跳转广告链接, 并提供微信分享功能.
2. 异步下载广告信息, 提高启动速度; 异步下载并保存广告和分享图片, 提高加载速度.
开发过程中, 使用了一些小技巧, 我会详细讲解注意的要点, 包括:
(1) 使用RxAndroid库, 在新线程上做异步下载广告信息.
(2) 使用Picasso库, 异步下载图片(Bitmap)并存储至本地.
(3) 使用原生Handler类, 实现计时器功能, 按秒跳转数字.
(4) 使用WebView视图, 加载广告链接, 并提供分享功能.
1. 下载广告
在欢迎页面中, 启动一个异步线程, 加载广告信息, 提高启动速度, 防止网速过慢导致切换卡顿.
// 异步广告信息 private void AsyncCheckInfo() { // 异步线程处理监听, 在新线程上监听, 发送到主线程 Observable<String> observable = Observable.create(new Observable.OnSubscribe<String>() { @Override public void call(Subscriber<? super String> subscriber) { subscriber.onNext(checkInfo()); subscriber.onCompleted(); } }).subscribeOn(Schedulers.newThread()).observeOn(AndroidSchedulers.mainThread()); // 成功回调 observable.subscribe(new Subscriber<String>() { @Override public void onCompleted() { Log.i(TAG, "onCompleted"); } @Override public void onError(Throwable e) { } @Override public void onNext(String s) { Log.i(TAG, "onNext"); } }); }
在新线程(newThread)中加载, 完成后发送到主线程(mainThread). 参考.
判断网络, 在有网的时候, 加载广告信息; 在无网的时候, 直接略过.
// 加载广告信息 public String checkInfo() { if (NetUtils.isNetworkConnected(ChunyuApp.getAppContext())) { UpdateUtils.checkDailyInfo(WelcomeActivity.this, mDailyRequestCallback); return "Begin to load info."; } else { return "Stop to load info"; } }
在UpdateUtils.checkDailyInfo
中, 解析广告请求的返回值. 如果包含广告信息, 则存储在首选项(SharedPreference)中, 下次启动广告直接读取; 如果不包含广告信息, 则设置无数据标记, 在使用时判定无广告.
最后调用回调接口mDailyRequestCallback
继续处理.
ArrayList<Advert> adverts = version.advert; if (adverts.size() > 0) { for (int i = 0; i < adverts.size(); ++i) { Advert advert = adverts.get(i); if (advert.Number == 1) { // Number等于0是广告 PedometerAdManager.getInstance().init(advert); } } } else { Log.e(TAG, "广告是空"); SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(ChunyuApp.getAppContext()); sp.edit().putBoolean(WelcomeActivity.FIRST_AD_IS_HAVE_PREFS, false).apply(); }
2. 存储图片
已经存储广告信息之后, 即可获得图片下载链接, 为了提高显示速度, 下载图片存储在本地. 因为下载属于网络请求, 需要异步处理, 本文使用Picasso库, 没有发明轮子.
// 日常信息回调 private final UpdateUtils.DailyRequestCallback mDailyRequestCallback = new UpdateUtils.DailyRequestCallback() { @Override public void operationExecutedSuccess() { if (mAdManager.getImageUrl() != null && !mAdManager.getImageUrl().isEmpty()) Picasso.with(WelcomeActivity.this). load(mAdManager.getImageUrl()).into(mAdImageTarget); if (mAdManager.getShareIcon() != null && !mAdManager.getShareIcon().isEmpty()) Picasso.with(WelcomeActivity.this). load(mAdManager.getShareIcon()).into(mAdShareImageTarget); } @Override public void operationExecutedFailed() { Log.e(TAG, "operationExecutedFailed"); } }; // 广告图片 private Target mAdImageTarget = new Target() { @Override public void onBitmapLoaded(Bitmap bitmap, Picasso.LoadedFrom from) { String path = FileUtility.savePic(bitmap); mPrefs.edit().putString(FIRST_AD_PATH_PREFS, path).apply(); } @Override public void onBitmapFailed(Drawable errorDrawable) { } @Override public void onPrepareLoad(Drawable placeHolderDrawable) { } }; // 分享Icon private Target mAdShareImageTarget = new Target() { @Override public void onBitmapLoaded(Bitmap bitmap, Picasso.LoadedFrom from) { String path = FileUtility.savePic(bitmap); mPrefs.edit().putString(FIRST_AD_SHARE_IMAGE_URL_PREFS, path).apply(); } @Override public void onBitmapFailed(Drawable errorDrawable) { } @Override public void onPrepareLoad(Drawable placeHolderDrawable) { } };
在页面暂停时, 移除Picasso的请求线程.
@Override protected void onPause() { MobclickAgent.onPause(this); handler.removeCallbacks(runnable); // 停止 Picasso.with(this).cancelRequest(mAdImageTarget); // 停止 Picasso.with(this).cancelRequest(mAdShareImageTarget); // 停止 super.onPause(); }
注意: 在Picasso中,
Target
是和ImageView控件
弱绑定, 在销毁ImageView时, 会随之销毁. 如果未提供ImageView控件
, 需要手动销毁请求, 如在onPause
中取消. 否则会出现下载异常. 参考.
3. 显示广告
首先Logo页显示LOGO_TIME
秒, 再判断显示引导(首次启动)
或显示广告
.
显示广告是使用存储在首选项(SharedPreference)中的数据, 图片使用本地资源解析, 提高显示速度.
// 显示启动信息 private void showLaunchInfo() { // 显示一段时间的主屏Logo new Handler().postDelayed(this::showAdInfo, LOGO_TIME); } // 显示广告信息 private void showAdInfo() { // 判断是否有广告 if (mPrefs.getBoolean(FIRST_AD_IS_HAVE_PREFS, false)) { Log.e(TAG, "包含广告"); String path = mPrefs.getString(FIRST_AD_PATH_PREFS, ""); if (!path.isEmpty()) { int time = mPrefs.getInt(FIRST_AD_TIME_PREFS, 0); Bitmap bitmap = BitmapFactory.decodeFile(path); Log.e(TAG, "time: " + time); showAdImage(bitmap, time); if (!NetUtils.isNetworkConnected(ChunyuApp.getAppContext())) { mIvWebImage.setClickable(false); } } else { gotoOtherActivity(); } } else { gotoOtherActivity(); } }
显示的广告使用
上次网络请求的存储数据
, 也可能是本次网络请求的
, 主要取决于在LOGO_TIME
时间中, 是否下载完成启动信息, 并存储至本地.
4. 广告计时器
在广告图片显示时, 提供倒计时器
, 按秒跳时, 提供跳过按钮
直接跳过广告.
// 显示广告 private void showAdImage(Bitmap bitmap, int time) { mIvWebImage.setVisibility(View.VISIBLE); mTvSkip.setVisibility(View.VISIBLE); mTvSkip.setOnClickListener(v -> gotoOtherActivity()); mIvBackground.setVisibility(View.INVISIBLE); mIvFirstLogo.setVisibility(View.INVISIBLE); mIvWebImage.setImageBitmap(bitmap); mAdTime = time + 2; handler.post(runnable); // 设置读秒 } // 设置读秒器 private int s = 0; // 时间Delay private final Handler handler = new Handler(); private final Runnable runnable = new Runnable() { @Override public void run() { // handler自带方法实现定时器 try { handler.postDelayed(this, 1000); if (s < 1) { s++; return; } if (s <= (mAdTime - 1)) { mTvSkip.setText(String.valueOf("跳过\n" + Integer.toString((mAdTime - 1) - (s++)) + "秒")); } // 计时器为0时, 开始跳转 if (s == mAdTime) { gotoOtherActivity(); } } catch (Exception e) { e.printStackTrace(); } } };
广告时间额外显示两秒, 提供页面跳转间隔, 前一秒后一秒, 保证广告时间充足.
在广告页跳转
或页面结束
时, 删除计时回调.
// 跳转到现实广告的视图 public void gotoShowAdView(View view) { NV.o(this, AdvertisementActivity.class); handler.removeCallbacks(runnable); finish(); }
@Override protected void onPause() { MobclickAgent.onPause(this); handler.removeCallbacks(runnable); // 停止 Picasso.with(this).cancelRequest(mAdImageTarget); // 停止 Picasso.with(this).cancelRequest(mAdShareImageTarget); // 停止 super.onPause(); }
本文使用
handler
类, 循环调用计时, 必须在离开页面时, 清除runnable
回调. 否则会遗忘线程泄露内存.
5. 链接页面
点击广告图片, 会跳转至广告链接, 根据参数设置全屏或者提供分享功能, 把链接分享至微信. 微信分享需要标题, 内容, 图标(Icon), 其中图片是从服务器下载后预存在本地.
/** * 广告Activity * <p> * Created by wangchenlong on 15/12/2. */ public class AdvertisementActivity extends PActivity { @SuppressWarnings("unused") private static final String TAG = "DEBUG-WCL: " + AdvertisementActivity.class.getSimpleName(); @Bind(R.id.advertise_pwv_container) PedoWebView mPwvContainer; @Bind(R.id.advertise_ll_back_home) LinearLayout mLlBackHome; @Bind(R.id.advertise_ll_send_session) LinearLayout mLlSendSession; @Bind(R.id.advertise_ll_send_timeline) LinearLayout mLlSendTimeline; @Bind(R.id.advertise_ll_action_bar) LinearLayout mLlActionBar; private SharedPreferences mPrefs; private int mFlag; // 判断分享地点 private static final int WECHAT_SESSION = 0; // 微信对话 private static final int WECHAT_TIMELINE = 1; // 朋友圈 @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_advertisement); ButterKnife.bind(this); mPrefs = PreferenceManager.getDefaultSharedPreferences(ChunyuApp.getAppContext()); ActionBar actionBar = getSupportActionBar(); if (actionBar != null) actionBar.setDisplayHomeAsUpEnabled(true); // 是否全屏 if (mPrefs.getBoolean(WelcomeActivity.FIRST_AD_IS_FULL_PREFS, false)) { mLlActionBar.setVisibility(View.GONE); } else { mLlBackHome.setOnClickListener(v -> { NV.o(this, PedometerActivity.class); finish(); }); // 是否分享 if (mPrefs.getBoolean(WelcomeActivity.FIRST_AD_IS_SHARE_PREFS, false)) { mLlSendSession.setOnClickListener(v -> { mFlag = WECHAT_SESSION; shareWechat(); }); mLlSendTimeline.setOnClickListener(v -> { mFlag = WECHAT_TIMELINE; shareWechat(); }); } else { mLlSendSession.setVisibility(View.GONE); mLlSendTimeline.setVisibility(View.GONE); } } mPwvContainer.loadUrl(mPrefs.getString(WelcomeActivity.FIRST_AD_URL_PREFS, "")); } // 分享到微信 public void shareWechat() { IWXAPI wxapi = WXAPIFactory.createWXAPI(ChunyuApp.getAppContext(), SNSConst.WX_APP_ID_ONLINE, true); wxapi.registerApp(SNSConst.WX_APP_ID_ONLINE); WXWebpageObject webpage = new WXWebpageObject(); webpage.webpageUrl = mPrefs.getString(WelcomeActivity.FIRST_AD_URL_PREFS, ""); WXMediaMessage msg = new WXMediaMessage(webpage); msg.title = mPrefs.getString(WelcomeActivity.FIRST_AD_SHARE_TITLE_PREFS, ""); msg.description = mPrefs.getString(WelcomeActivity.FIRST_AD_SHARE_CONTENT_PREFS, ""); String path = mPrefs.getString(WelcomeActivity.FIRST_AD_SHARE_IMAGE_URL_PREFS, ""); if (!path.isEmpty()) { Bitmap bitmap = BitmapFactory.decodeFile(path); if (bitmap != null) { msg.setThumbImage(bitmap); } else { msg.setThumbImage(BitmapFactory.decodeResource(getResources(), R.drawable.icon)); } SendMessageToWX.Req req = new SendMessageToWX.Req(); req.transaction = String.valueOf(System.currentTimeMillis()); req.message = msg; req.scene = ((mFlag == 0) ? SendMessageToWX.Req.WXSceneSession : SendMessageToWX.Req.WXSceneTimeline); wxapi.sendReq(req); } } @Override public void onBackPressed() { if (mPwvContainer.canGoBack()) { mPwvContainer.goBack(); } else { NV.o(this, PedometerActivity.class); finish(); } } }
调用后退按钮(onBackPressed)
: 在网页跳转多页时, 返回上一页; 在首页时, 退出广告页面, 跳转主页. 微信分享的图标(Icon), 最好使用方形全图, 否则透明部分会被黑色替代, 服务器提供图片时需要注意.
最终效果:
OK, 广告页面开发完成了, 可以开心的赚钱了! Enjoy It.
来自: http://blog.csdn.net//caroline_wendy/article/details/50183487