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

前言

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

Splash页面三秒跳转和动态下载背景图_第1张图片
开屏图

需求分析

  • 开屏图背景和跳转地址可在后台配置,跳转地址可为空,客户端请求接口判断后台是否配置,当没有数据时,使用默认图片
  • 页面默认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();
    }
}

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



    
    

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 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()方法中有所体现

总结

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

你可能感兴趣的:(Splash页面三秒跳转和动态下载背景图)