Splash页面三秒跳转和动态下载背景图

前言

市场上的应用大多的Splash页都添加了动态背景图的功能,不知道这种行为的专业名词是什么,在我们公司里把这个叫做开屏图,这些都不重要,名称只是方便我们描述,所以下文中都统称为开屏图。它的样式相信大家都不陌生见下图:

开屏图

需求分析

  • 开屏图背景和跳转地址可在后台配置,跳转地址可为空,客户端请求接口判断后台是否配置,当没有数据时,使用默认图片
  • 页面默认3s倒计时,点击跳过结束倒计时,直接跳过
  • 当配置了跳转地址时,点击背景跳转至对应的web页
  • 动态替换,当后台配置多张图片时,在wifi环境下全部下载,下次打开App时,随机展示一张,在非wifi环境下只随机下载一张,下次打开App,展示最新图片
  • 该web跳转符合业务流程(每个公司各有特殊,至于打开web页的流程不在此展开描述)

详细设计

本项目下载图片使用的是传统图片加载库Android-Universal-Image-Loader,当然还可以选择其他图片库,但实现思路基本不变。

准备工作
“跳过”按钮属于自定义View范畴,需要自己实现,代码如下:

/**
 * Created by zs on 2017/6/8.
 *
 * 圆形进度View
 */

public class RoundProgressView extends View {

    /** 画笔 */
    private Paint mPaint;

    /** 字体大小 */
    private float mTextSize;

    /** 圆环宽度 */
    private float mRoundWidth;

    /** 圆环颜色 */
    private int mRoundColor;

    /** 圆环进度颜色 */
    private int mRoundProgressColor;

    /** 圆环进度 */
    private int mProgress;

    /** 绘制圆弧对象 */
    private RectF mOval;

    public RoundProgressView(Context context) {
        this(context, null);
    }

    public RoundProgressView(Context context, AttributeSet attrs) {
        this(context,attrs,0);
    }

    public RoundProgressView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        init();
    }

    /**
     * 初始化
     */
    private void init() {
        setBackgroundResource(R.drawable.shape_gray_circle);
        mPaint = new Paint();
        mTextSize = ScreenUtils.sp2px(getResources(), 14);
        mRoundWidth = ScreenUtils.dp2px(getResources(), 2);
        mRoundColor = getResources().getColor(R.color.color_10000000);
        mRoundProgressColor = Color.WHITE;
        mOval = new RectF();
    }

    @Override
    protected void onDraw(Canvas canvas) {

        /*第一步:绘制最外层圆环*/
        int center = getWidth() / 2;
        mPaint.setAntiAlias(true);
        mPaint.setColor(mRoundColor);
        mPaint.setStrokeWidth(mRoundWidth);
        mPaint.setStyle(Paint.Style.STROKE);
        int radius = (int) (center - mRoundWidth / 2);
        canvas.drawCircle(center, center, radius, mPaint);

        /*第二步:绘制正中间的文本*/
        mPaint.setTextSize(mTextSize);
        float textWidth = mPaint.measureText("跳过");
        mPaint.setColor(Color.WHITE);
        mPaint.setStrokeWidth(0);
        canvas.drawText("跳过", center - textWidth / 2, center + mTextSize / 3, mPaint);

        /*第三步:绘制圆弧*/
        mOval.set(center - radius, center - radius, center + radius, center + radius);
        mPaint.setColor(mRoundProgressColor);
        mPaint.setStrokeWidth(mRoundWidth);
        mPaint.setStyle(Paint.Style.STROKE);
        canvas.drawArc(mOval, -90, 360 * mProgress / 100, false, mPaint);
    }

    /**
     * 设置圆环进度
     *
     * @param progress 进度
     */
    public void setProgress(int progress){
        this.mProgress = progress;
        if(progress > 100){
            this.mProgress = 100;
        }
        postInvalidate();
    }
}

当然,上面的代码也可以自定义属性来增加扩展性,由于这个组件在本项目中可复用性较低,暂没有自定义属性。使用起来也比较简单,采用属性动画来改变进度:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout 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">
    <ImageView
        android:id="@+id/iv_splash"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@drawable/splash"
        android:scaleType="fitXY"/>
    <com.xxx.xx.RoundProgressView
        android:id="@+id/round_progress"
        android:layout_width="@dimen/dimen_88"
        android:layout_height="@dimen/dimen_88"
        android:layout_alignParentEnd="true"
        android:layout_alignParentRight="true"
        android:layout_marginEnd="@dimen/dimen_30"
        android:layout_marginRight="@dimen/dimen_30"
        android:layout_marginTop="@dimen/dimen_40"
        android:visibility="gone"
        tools:visibility="visible"/>
</RelativeLayout>
mValueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
    @Override
    public void onAnimationUpdate(ValueAnimator animation) {
        int progress = (int) animation.getAnimatedValue();
        mRoundProgress.setProgress(progress);
    }
});
mValueAnimator.setDuration(3000);
mValueAnimator.start();

下载图片
请求接口,接口返回图片信息,使用ImageLoader下载图片,见以下代码:

/**
 * Created by zs on 2017/6/9.
 *
 * 下载开屏页图片
 */

public class SplashImageDownload {

    /** 本地存储的最终图片数据 */
    private SplashAdResp.DataResp.ContentResp mSplashAdContent = new SplashAdResp.DataResp.ContentResp();

    /** 图片的数量 */
    private int mImageSize;

    /** 已加载的图片数量 */
    private int mLoadedImageSize;

    /** 服务器端图片集合 */
    private List<SplashAdResp.DataResp.ContentResp.ItemResp> mNetItemList;

    public SplashImageDownload(SplashAdResp.DataResp.ContentResp contentResp){
        mImageSize = 0;
        mLoadedImageSize = 0;
        this.startDownload(contentResp);
    }

    /**
     * 开始下载图片信息
     *
     * @param contentResp 图片信息
     */
    private void startDownload(SplashAdResp.DataResp.ContentResp contentResp) {
        if(contentResp == null){
            LogUtil.D("splash_down: 加载图片时服务端图片信息数据异常");
            return;
        }

        mNetItemList = contentResp.advertisementImages;
        if(mNetItemList == null){
            LogUtil.D("splash_down: 加载图片时服务端图片信息数据异常");
            return;
        }

        if(mNetItemList.isEmpty()){
            //清空本地存储数据
            PreferencesUtils.clearString(getContext(),Constants.SPLASH_AD_RANDOM_SHOW);
            LogUtil.D("splash_down: 加载图片时服务端图片信息数据为空");
            return;
        }

        mSplashAdContent.advertisementImages = new Vector<>();
        DisplayImageOptions options = new DisplayImageOptions.Builder().cacheOnDisk(true).build();

        //非wifi环境只随机下载一张图片
        if(!NetWorkUtils.isWifiConnected()){
            mImageSize = 1;
            Random random = new Random();
            final int index = random.nextInt(mNetItemList.size());
            ImageLoader.getInstance().loadImage(mNetItemList.get(index).imageUrl,options,new SimpleImageLoadingListener(){
                @Override
                public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) {
                    super.onLoadingComplete(imageUri, view, loadedImage);
                    mSplashAdContent.advertisementImages.add(mNetItemList.get(index));
                    LogUtil.D("splash_down: 非wifi加载成功 --->" + imageUri);
                    setTempImageSize();
                }

                @Override
                public void onLoadingFailed(String imageUri, View view, FailReason failReason) {
                    super.onLoadingFailed(imageUri, view, failReason);
                    LogUtil.D("splash_down: 非wifi加载失败地址 --->" + imageUri);
                    setTempImageSize();
                }
            });
            return;
        }

        //保存图片集合大小
        mImageSize = mNetItemList.size();

        for (int i = 0; i < mNetItemList.size(); i++) {
            final int itemIndex = i;
            ImageLoader.getInstance().loadImage(mNetItemList.get(i).imageUrl,options,new SimpleImageLoadingListener(){
                @Override
                public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) {
                    super.onLoadingComplete(imageUri, view, loadedImage);
                    mSplashAdContent.advertisementImages.add(mNetItemList.get(itemIndex));
                    LogUtil.D("splash_down: 加载成功索引值 --->" + itemIndex);
                    setTempImageSize();
                }

                @Override
                public void onLoadingFailed(String imageUri, View view, FailReason failReason) {
                    super.onLoadingFailed(imageUri, view, failReason);
                    LogUtil.D("splash_down: 加载失败索引值 --->" + itemIndex);
                    LogUtil.D("splash_down: 加载失败地址 --->" + imageUri);
                    setTempImageSize();
                }
            });
        }
    }

    /**
     * 标记下载图片数量
     */
    private void setTempImageSize(){
        mLoadedImageSize++;

        //已加载完成
        if (mLoadedImageSize == mImageSize) {
            downloadFinished();//下载完成保存图片信息
        }

    }

    /**
     * 下载完成保存图片信息
     */
    private void downloadFinished() {

        boolean isSuccess =
                mSplashAdContent != null && mSplashAdContent.advertisementImages != null &&
                        mSplashAdContent.advertisementImages.size() > 0;
        if (isSuccess) {
            Random random = new Random();
            int index = random.nextInt(mSplashAdContent.advertisementImages.size());
            String randomJson = new Gson().toJson(mSplashAdContent.advertisementImages.get(index));
            PreferencesUtils.putString(getContext(), Constants.SPLASH_AD_RANDOM_SHOW, randomJson);
            LogUtil.D("splash_down: 存储显示图片数据成功" + randomJson);
        }
    }

整体的思路就是:

  1. 如果服务端返回的数据为空,则清空缓存中的数据,下次打开App,不再展示
  2. 根据网络环境的不同下载图片,wifi全部下载,非wifi环境只随机下载一张
  3. 设置一个全局变量,统计下载图片的个数,或成功或失败,当下载的图片个数(下载成功+下载失败) = 服务端返回的图片个数时,证明图片下载结束,则保存本次下载成功的图片信息
  4. 保存图片的信息时,只随机保存一张,下次直接展示
  5. 由于图片加载库本身就有缓存,所以没有判断服务端返回的图片地址在本地有没有缓存,采用直接加载的方式,让框架帮忙处理该图片是否需要下载,在这里也可先判断有无下载过该张图片,没有则下载,有则不下载,采用两种方式均可。
  6. 在这里需要注意的是,当图片地址为空时,也会走到图片下载成功处理中,所以在这里需要注意,本项目采用的是在显示的时候,增加判断,参见下面代码

显示图片

/**
 * update by zs on 17/6/6 
 */
public class SplashActivity extends BaseFragmentActivity {

    /** 倒计时进度View */
    private RoundProgressView mRoundProgress;

    /** splash背景 */
    private ImageView mIvSplash;

    /** 属性动画 */
    private ValueAnimator mValueAnimator;

    /** 动画是否真正结束 */
    private boolean mIsRealEndAnimator = true;

    /** 页面是否真正进入后台 */
    private boolean mIsRealIntoBackground = true;


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        requestWindowFeature(Window.FEATURE_NO_TITLE);
        setContentView(R.layout.activity_splash);

        mRoundProgress = (RoundProgressView) findViewById(R.id.round_progress);
        mIvSplash = (ImageView) findViewById(R.id.iv_splash);

        try {
            showSplashBackground();//展示开屏图背景
        } catch (Exception e) {
            startPage();//发生异常跳转对应页面
        }
    }

    @Override
    protected void onRestart() {
        super.onRestart();
        startPage();
    }

    @Override
    protected void onStop() {
        super.onStop();
        if(mIsRealIntoBackground){
            endAnimation();
        }
    }

    /**
     * 展示开屏图背景
     */
    private void showSplashBackground() {
        //获取本地存储图片信息
        String json = PreferencesUtils.getString(getApplicationContext(), Constants.SPLASH_AD_RANDOM_SHOW);
        final SplashAdResp.DataResp.ContentResp.ItemResp itemResp = new Gson().fromJson(json, SplashAdResp.DataResp.ContentResp.ItemResp.class);

        //为空判断
        if(itemResp == null || TextUtils.isEmpty(itemResp.imageUrl)){
            postDelay();//存储广告图片信息为空
            LogUtil.D("splash_down: 随机取出的图片信息异常");
            return;
        }

        //查询本地有无存储该图片
        File file = ImageLoader.getInstance().getDiskCache().get(itemResp.imageUrl);
        if(file == null || !file.exists()){
            LogUtil.D("splash_down: 本地没有存储该图片");
            postDelay();
            return;
        }

        LogUtil.D("splash_down: 随机取出的图片信息 " + itemResp.toString());

        //加载图片信息
        DisplayImageOptions options = new DisplayImageOptions.Builder()
                .showImageForEmptyUri(R.drawable.splash)//url为空
                .showImageOnFail(R.drawable.splash).build();//加载异常
        ImageLoader.getInstance().displayImage(itemResp.imageUrl,mIvSplash, options, new SimpleImageLoadingListener(){
                    @Override
                    public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) {
                        super.onLoadingComplete(imageUri, view, loadedImage);
                        loadImageSuccess(itemResp.url, itemResp.title);
                    }

                    @Override
                    public void onLoadingFailed(String imageUri, View view, FailReason failReason) {
                        super.onLoadingFailed(imageUri, view, failReason);
                        postDelay();//加载失败本地splash页延迟操作
                        LogUtil.D("splash_down: 加载开屏图失败");
                    }
                });
    }

    /**
     * 延迟操作
     */
    private void postDelay() {
        new Handler().postDelayed(new Runnable() {
            public void run() {
                startPage();
            }
        }, 3000);
    }

    /**
     * 延迟完成后跳转对应页面
     */
    private void startPage() {
        //启动页面回调onStop方法, 此时页面不是按Home键进入后台
        mIsRealIntoBackground = false ;

        //to do your work
    }

    /**
     * 加载广告图片成功处理
     *
     * @param url 挑战地址
     * @param title 跳转网页title
     */
    private void loadImageSuccess(final String url, final String title){
        mValueAnimator = ValueAnimator.ofInt(1, 100);

        if(!TextUtils.isEmpty(url)){
            mIvSplash.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    endAnimation();//结束动画
                    // 跳转web页面
                    finish();
                }
            });
        }

        mRoundProgress.setVisibility(View.VISIBLE);
        mRoundProgress.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                endAnimation();//结束动画
                startPage();//点击 跳转按钮 跳转对应页面
            }
        });

        mValueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                int progress = (int) animation.getAnimatedValue();
                mRoundProgress.setProgress(progress);
            }

        });

        mValueAnimator.addListener(new Animator.AnimatorListener() {
            @Override
            public void onAnimationStart(Animator animation) {
                LogUtil.D("splash_down: 倒计时动画开始");
            }

            @Override
            public void onAnimationEnd(Animator animation) {
                LogUtil.D("splash_down: 倒计时动画结束");
                if(mIsRealEndAnimator){
                    startPage();//动画执行结束跳转对应页面
                }
            }

            @Override
            public void onAnimationCancel(Animator animation) {
                LogUtil.D("splash_down: 倒计时动画取消");
            }

            @Override
            public void onAnimationRepeat(Animator animation) {
                LogUtil.D("splash_down: 倒计时动画重复");
            }
        });
        mValueAnimator.setDuration(3000);
        mValueAnimator.start();
    }

    /**
     * 结束动画
     */
    private void endAnimation(){
        mIsRealEndAnimator = false;
        if (mValueAnimator != null) {
            mValueAnimator.end();
        }
    }
}

整体的思路就是:

  1. 从本地缓存中取出图片信息,判断数据是否合法
  2. 在图片缓存中查找该图片有无缓存
  3. 各种参数不合法以及加载图片失败和未知异常,都直接执行正常的业务流程
  4. 属性动画设置进度,且监听该动画,动画结束时,执行正常业务流程
  5. 点击跳过或背景,结束动画,执行对应流程

注意事项

  1. 手动结束动画的时候,如上面的endAnimation()方法,也最终会走到动画的onAnimationEnd回调中,这样会导致打开页面时,会打开两次页面,所以使用mIsRealEndAnimator变量来区别是否时正常的结束动画
  2. 在打开开屏页的时候,这个时候立即按Home键,应用进入后台中,这个时候过三秒应用又会重新打开,是因为应用进入后台时我们没有结束动画,在这里需要特殊处理下,使用相同的思路,利用mIsRealIntoBackground变量来区别应用是否真正进入后台,在onStop() startPage()onRestart()方法中有所体现

总结

同一个需求实现的方式可能有很多,但最终的目的只有一个,感谢你耐心的看完了整篇文章,希望可以给你有参考的价值。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,117评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,328评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,839评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,007评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,384评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,629评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,880评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,593评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,313评论 1 243
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,575评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,066评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,392评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,052评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,082评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,844评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,662评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,575评论 2 270

推荐阅读更多精彩内容