前言
市场上的应用大多的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);
}
}
整体的思路就是:
- 如果服务端返回的数据为空,则清空缓存中的数据,下次打开App,不再展示
- 根据网络环境的不同下载图片,wifi全部下载,非wifi环境只随机下载一张
- 设置一个全局变量,统计下载图片的个数,或成功或失败,当下载的图片个数(下载成功+下载失败) = 服务端返回的图片个数时,证明图片下载结束,则保存本次下载成功的图片信息
- 保存图片的信息时,只随机保存一张,下次直接展示
- 由于图片加载库本身就有缓存,所以没有判断服务端返回的图片地址在本地有没有缓存,采用直接加载的方式,让框架帮忙处理该图片是否需要下载,在这里也可先判断有无下载过该张图片,没有则下载,有则不下载,采用两种方式均可。
- 在这里需要注意的是,当图片地址为空时,也会走到图片下载成功处理中,所以在这里需要注意,本项目采用的是在显示的时候,增加判断,参见下面代码
显示图片
/**
* 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();
}
}
}
整体的思路就是:
- 从本地缓存中取出图片信息,判断数据是否合法
- 在图片缓存中查找该图片有无缓存
- 各种参数不合法以及加载图片失败和未知异常,都直接执行正常的业务流程
- 属性动画设置进度,且监听该动画,动画结束时,执行正常业务流程
- 点击跳过或背景,结束动画,执行对应流程
注意事项
- 手动结束动画的时候,如上面的
endAnimation()
方法,也最终会走到动画的onAnimationEnd
回调中,这样会导致打开页面时,会打开两次页面,所以使用mIsRealEndAnimator
变量来区别是否时正常的结束动画 - 在打开开屏页的时候,这个时候立即按Home键,应用进入后台中,这个时候过三秒应用又会重新打开,是因为应用进入后台时我们没有结束动画,在这里需要特殊处理下,使用相同的思路,利用
mIsRealIntoBackground
变量来区别应用是否真正进入后台,在onStop()
startPage()
和onRestart()
方法中有所体现
总结
同一个需求实现的方式可能有很多,但最终的目的只有一个,感谢你耐心的看完了整篇文章,希望可以给你有参考的价值。