1、概述
??啟動頁廣告幾乎無處不在,大部分App都有它的身影,那么它的處理邏輯到底是什么樣的呢?我們拭目以待。

2、實現(xiàn)流程
1、啟動頁
??啟動頁幾乎都會存在拉伸變形和黑白屏這兩種情況,要徹底解決這兩個問題并不簡單,當(dāng)然,在一些硬性前提下還是可以做到的,首先,啟動頁圖片不要太復(fù)雜且非git動畫,展示的內(nèi)容不要太多、一兩塊區(qū)域即可,類似QQ音樂、新浪微博和QQ這樣的啟動頁、只需要在xml中通過<layer-list></layer-list>、設(shè)置背景為白色、將內(nèi)容切圖(logo)堆疊起來即可、最后使用xml作為啟動頁主題的背景就可以避免以上兩個大問題了。



2、申請權(quán)限
??權(quán)限申請這里使用的是第三方開源庫AndPermission,這里需要注意的是系統(tǒng)版本大于等于6.0以上才能申請相應(yīng)的權(quán)限,否則,會出現(xiàn)一些異常情況。
3、接口獲取廣告內(nèi)容(是否展示廣告、廣告下載鏈接)
??這個步驟沒什么好說的。
4、圖片下載
Retrofit初始化
OkHttpClient fileClient = new OkHttpClient.Builder()
.readTimeout(1, java.util.concurrent.TimeUnit.MINUTES)
.cache(cache).build();
mRetrofit = mRetrofit.newBuilder()
.client(fileClient)
.baseUrl(loadImage)
.build();
??mRetrofit初始化這里,之前由于OkHttpClient添加攔截器,導(dǎo)致圖片下載失敗,去掉攔截器即可。具體原因未知
api聲明
public interface FileApi {
/**
*
*
* @return
*/
@GET
@Streaming
Observable<ResponseBody> getSplashBanner(@Url String url);
}
啟動頁廣告背景圖文件夾
/storage/sdcard/Android/data/com.xxx.xxxx/files/Download/banner
mBannerDir = new File(mContext.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "banner");
if (!mBannerDir.exists()) {
mBannerDir.mkdirs();
}
加載啟動頁廣告
??使用下載鏈接中的后綴作為文件名進(jìn)行存儲,下載前根據(jù)名字判斷啟動頁廣告是否已經(jīng)存在,優(yōu)先使用緩存圖片。
/**
* API下載啟動頁廣告
*/
private void loadBanner(String url) {
String[] urlArray = url.split("/");
final String fileName = urlArray[urlArray.length - 1];
File bannerFile = new File(FileUtils.mBannerDir, fileName);
if (bannerFile.exists()) {
showSplashBanner(bannerFile);
return;
}
Http.http.createDownloadImage(FileApi.class).
getSplashBanner(url)
.subscribeOn(Schedulers.io())//請求網(wǎng)絡(luò) 在調(diào)度者的io線程
.observeOn(Schedulers.io()) //指定線程保存文件
.observeOn(Schedulers.computation())
.map(new Func1<ResponseBody, Boolean>() {
@Override
public Boolean call(ResponseBody responseBody) {
return writeFileToSDCard(responseBody, fileName);
}
})
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Action1<Object>() {
@Override
public void call(Object o) {
File futureStudioIconFile = new File(FileUtils.mBannerDir, fileName);
showSplashBanner(futureStudioIconFile);
}
});
}
保存圖片
/**
* @param body
* @param fileName(含有后綴)
* @return
*/
private boolean writeFileToSDCard(ResponseBody body, String fileName) {
try {
File futureStudioIconFile = new File(FileUtils.mBannerDir, fileName);
OutputStream outputStream = null;
try {
futureStudioIconFile.deleteOnExit();
futureStudioIconFile.createNewFile();
outputStream = new FileOutputStream(futureStudioIconFile);
/*使用工具類對圖片進(jìn)行處理*/
mTargetBitmap = BitmapUtils.scaleImage(body.bytes(), outSize.x, outSize.y);
mTargetBitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream);
outputStream.flush();
return true;
} catch (IOException e) {
return false;
} finally {
if (outputStream != null) {
outputStream.close();
}
}
} catch (IOException e) {
return false;
}
}
圖片縮放裁剪工具類
public class BitmapUtils {
private static final String TAG = "BitmapUtils";
/**
* 等比例縮放圖片
*
* @param banner 圖片字節(jié)數(shù)組
* @param targetWidth 目標(biāo)寬度
* @param targetHeight 目標(biāo)高度
* @return
*/
public static Bitmap scaleImage(byte[] banner, int targetWidth, int targetHeight) {
Bitmap originBitmap = BitmapFactory.decodeByteArray(banner, 0, banner.length);
int w = originBitmap.getWidth();
int h = originBitmap.getHeight();
Log.i(TAG, "原圖高寬: " + h + "*" + w);
Log.i(TAG, "屏幕高寬: " + targetHeight + "*" + targetWidth);
float hRatio = targetHeight / (h * 1.0f);//高度縮放hRatio才能鋪滿屏幕
float wRatio = targetWidth / (w * 1.0f);//寬度縮放wRatio才能鋪滿屏幕
float finalRatio = 0;
/*1、獲取縮放比例*/
if (hRatio >= 1.0f && wRatio >= 1.0f) {
/*圖片小,放大才能鋪滿屏幕*/
finalRatio = hRatio > wRatio ? hRatio : wRatio;
} else if (hRatio >= 1f && wRatio < 1.0f) {
/*高度需要放大、寬度需要縮小才能鋪滿屏幕,主流機(jī)型不存在該情況,鋪滿為主要目標(biāo),繼續(xù)放大*/
finalRatio = hRatio;
} else if (hRatio < 1f && wRatio >= 1.0f) {
/*高度需要縮小、寬度需要放大才能鋪滿屏幕,主流機(jī)型不存在該情況,鋪滿為主要目標(biāo),繼續(xù)放大*/
finalRatio = wRatio;
} else {
/*圖片太大需要縮小才能鋪滿屏幕*/
finalRatio = hRatio > wRatio ? hRatio : wRatio;
}
/*不需要縮放*/
if (finalRatio == 1.0f) {
return originBitmap;
}
Bitmap targetBitmap = null;
/*2、縮放后的圖片*/
Bitmap waitCropBitmap = scaleBitmap(originBitmap, finalRatio);
Log.i(TAG, "裁剪后的高寬: " + waitCropBitmap.getHeight() + "*" + waitCropBitmap.getWidth());
/*3、裁剪圖片*/
if (waitCropBitmap.getHeight() > targetHeight) {
Log.i(TAG, "scaleImage: 高度裁剪");
int cropH = waitCropBitmap.getHeight() - targetHeight;
targetBitmap = cropVertical(waitCropBitmap, cropH);
} else {
Log.i(TAG, "scaleImage: 寬度裁剪");
int cropW = waitCropBitmap.getWidth() - targetWidth;
targetBitmap = cropHorizontalBitmap(waitCropBitmap, cropW);
}
/*4、回收圖片*/
if (originBitmap != null) {
originBitmap.recycle();
originBitmap = null;
}
if (waitCropBitmap != null) {
waitCropBitmap.recycle();
waitCropBitmap = null;
}
return targetBitmap;
}
/**
* 按比例縮放圖片
*
* @param origin 原圖
* @param ratio 比例
* @return 新的bitmap
*/
private static Bitmap scaleBitmap(Bitmap origin, float ratio) {
Log.i(TAG, "ratio: " + ratio);
if (origin == null) {
return null;
}
int width = origin.getWidth();
int height = origin.getHeight();
Matrix matrix = new Matrix();
matrix.preScale(ratio, ratio);
Bitmap newBM = Bitmap.createBitmap(origin, 0, 0, width, height, matrix, false);
if (newBM.equals(origin)) {
return newBM;
}
origin.recycle();
return newBM;
}
/**
* 裁剪頭部區(qū)域
*
* @param bitmap 原圖
* @param height
* @return 裁剪后的圖像
*/
private static Bitmap cropVertical(Bitmap bitmap, int height) {
Log.i(TAG, "cropVertical: height/2=" + (height / 2));
return Bitmap.createBitmap(bitmap, 0, height / 2, bitmap.getWidth(), bitmap.getHeight() - height);
}
/**
* 裁剪頭部區(qū)域
*
* @param bitmap 原圖
* @param width
* @return 裁剪后的圖像
*/
private static Bitmap cropHorizontalBitmap(Bitmap bitmap, int width) {
Log.i(TAG, "cropVertical: width/2=" + (width / 2));
return Bitmap.createBitmap(bitmap, width / 2, 0, bitmap.getWidth() - width, bitmap.getHeight());
}
}
3、高仿酷狗跳轉(zhuǎn)控件(SkipView)
1、需求分析
1、圓形背景顏色自定義、半徑自定義
2、倒計時時間自定義、圓環(huán)寬度自定義、圓環(huán)顏色自定義
3、文字大小、顏色和內(nèi)容自定義
2、屬性說明
| 屬性名 | 默認(rèn)值 | 備注 |
|---|---|---|
| progress_and_circle_distance | 10 | 圓環(huán)與圓形背景的距離 |
| progress_and_text_distance | 年齡 | 圓環(huán)與文字的間距 |
| progress_width | 6 | 圓環(huán)的寬度 |
| text_size | 20 | 文字大小 |
| progress_color | Color.WHITE | 倒計時圓環(huán)顏色 |
| circle_color | Color.parseColor("#38342e") | 圓形背景顏色 |
| text_color | Color.WHITE | 文本顏色 |
| text | 跳過 | 文本內(nèi)容 |
3、實現(xiàn)流程
attrs.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="SkipView">
<attr name="progress_and_circle_distance" format="dimension|reference" />
<attr name="progress_and_text_distance" format="dimension|reference" />
<attr name="progress_width" format="dimension|reference" />
<attr name="text_size" format="dimension|reference" />
<attr name="progress_color" format="color|reference" />
<attr name="circle_color" format="color|reference" />
<attr name="text_color" format="color|reference" />
<attr name="text" format="string|reference" />
</declare-styleable>
</resources>
自定義控件
public class SkipView extends View {
private Paint mCirclePaint;
private Paint mTextPaint;
private int mCircleColor = Color.parseColor("#38342e");
private float mTextSize = 20;
private String mText = "跳過";
private int mTextColor = Color.WHITE;
private float mProgressWidth = 6;
private int mProgressColor = Color.WHITE;
private float mProAndCircleDistance = 10;
private float mProAndTextDistance = 10;
private Rect mTextRect = new Rect();
private float mBaseYOffset = 0;
private float mBaseY = 0;
private Paint mProgressPaint;
private RectF mProgressRectF = new RectF();
private ValueAnimator mValueAnimator;
private float mCurPro;
private long mSecond = 3;
public SkipView(Context context) {
this(context, null);
}
public SkipView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public SkipView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SkipView, 0, 0);
mCircleColor = a.getColor(R.styleable.SkipView_circle_color, mCircleColor);
mTextSize = a.getDimension(R.styleable.SkipView_text_size, mTextSize);
mText = a.getString(R.styleable.SkipView_text);
if (TextUtils.isEmpty(mText)) {
mText = "跳過";
}
mTextColor = a.getColor(R.styleable.SkipView_text_color, mTextColor);
mProgressWidth = a.getDimension(R.styleable.SkipView_progress_width, mProgressWidth);
mProgressColor = a.getColor(R.styleable.SkipView_progress_color, mProgressColor);
mProAndCircleDistance = a.getDimension(R.styleable.SkipView_progress_and_circle_distance, mProAndCircleDistance);
mProAndTextDistance = a.getDimension(R.styleable.SkipView_progress_and_text_distance, mProAndTextDistance);
a.recycle();
/*圓背景*/
mCirclePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mCirclePaint.setColor(mCircleColor);
/*文字相關(guān)*/
mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mTextPaint.setColor(mTextColor);
mTextPaint.setTextSize(mTextSize);
mTextPaint.getTextBounds(mText, 0, mText.length(), mTextRect);
Paint.FontMetrics fontMetrics = mTextPaint.getFontMetrics();
mBaseYOffset = (fontMetrics.bottom - fontMetrics.top) / 2 - fontMetrics.bottom;
/*圓弧*/
mProgressPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mProgressPaint.setStrokeCap(Paint.Cap.ROUND);
mProgressPaint.setStrokeWidth(mProgressWidth);
mProgressPaint.setColor(mProgressColor);
mProgressPaint.setStyle(Paint.Style.STROKE);
mValueAnimator = new ValueAnimator();
mValueAnimator.setDuration(mSecond * 1000);
mValueAnimator.setFloatValues(-360, 0);
mValueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mCurPro = ((float) animation.getAnimatedValue());//0~360
invalidate();
}
});
mValueAnimator.setInterpolator(new LinearInterpolator());
mValueAnimator.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
}
@Override
public void onAnimationEnd(Animator animation) {
if (onTimeOutListener != null) {
onTimeOutListener.onTimeOut();
}
}
@Override
public void onAnimationCancel(Animator animation) {
}
@Override
public void onAnimationRepeat(Animator animation) {
}
});
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int width = (int) ((mProAndCircleDistance + mProgressWidth + mProAndTextDistance) * 2 + mTextRect.width());
setMeasuredDimension(width, width);
mBaseY = getMeasuredHeight() / 2f + mBaseYOffset;
mProgressRectF.left = mProAndCircleDistance + mProgressWidth / 2f;
mProgressRectF.top = mProAndCircleDistance + mProgressWidth / 2f;
mProgressRectF.right = width - mProgressRectF.left;
mProgressRectF.bottom = width - mProgressRectF.top;
}
@Override
protected void onDraw(Canvas canvas) {
float halfOfWidth = getMeasuredWidth() / 2f;
canvas.drawCircle(halfOfWidth, halfOfWidth, halfOfWidth, mCirclePaint);
canvas.drawText(mText, halfOfWidth - mTextRect.width() / 2f, mBaseY, mTextPaint);
canvas.drawArc(mProgressRectF, -90, mCurPro, false, mProgressPaint);
}
public void start(long second) {
mValueAnimator.setDuration(second * 1000);
mValueAnimator.start();
}
OnTimeOutListener onTimeOutListener;
public void setOnTimeOutListener(OnTimeOutListener onTimeOutListener) {
this.onTimeOutListener = onTimeOutListener;
}
public interface OnTimeOutListener {
void onTimeOut();
}
private static final String TAG = "SkipView";
/**
* Activity銷毀時,保證動畫銷毀
*/
public void onDestroy() {
if (mValueAnimator != null) {
Log.i(TAG, "移除所有監(jiān)聽器: ");
mValueAnimator.removeAllListeners();
mValueAnimator.cancel();
mValueAnimator = null;
}
}
}
繪制過程中,有兩點技巧需要牢記。
1、進(jìn)度的值由-360~0進(jìn)行變化
mValueAnimator.setFloatValues(-360, 0);
2、-90意思是在圓環(huán)的中心點的正上方開始繪制、mCurPro的意思是繪制的度數(shù)、正值順時針繪制、負(fù)值逆時針繪制
canvas.drawArc(mProgressRectF, -90, mCurPro, false, mProgressPaint);
4、參考教程
1、https://www.jb51.net/article/130850.htm
2、https://blog.csdn.net/yanzhenjie1003/article/details/52503533