开发首屏广告(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.

©️2020 CSDN 皮肤主题: 技术黑板 设计师:CSDN官方博客 返回首页