自定义View-启动页广告
1、概述
启动页广告几乎无处不在,大部分App都有它的身影,那么它的处理逻辑到底是什么样的呢?我们拭目以待。
banner.gif
2、实现流程
1、启动页
启动页几乎都会存在拉伸变形和黑白屏这两种情况,要彻底解决这两个问题并不简单,当然,在一些硬性前提下还是可以做到的,首先,启动页图片不要太复杂且非git动画,展示的内容不要太多、一两块区域即可,类似QQ音乐、新浪微博和QQ这样的启动页、只需要在xml中通过<layer-list></layer-list>、设置背景为白色、将内容切图(logo)堆叠起来即可、最后使用xml作为启动页主题的背景就可以避免以上两个大问题了。
qq音乐启动页.png
新浪启动页.png QQ启动页.jpg
2、申请权限
权限申请这里使用的是第三方开源库AndPermission,这里需要注意的是系统版本大于等于6.0以上才能申请相应的权限,否则,会出现一些异常情况。
3、接口获取广告内容(是否展示广告、广告下载链接)
这个步骤没什么好说的。
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添加拦截器,导致图片下载失败,去掉拦截器即可。具体原因未知
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();
}
加载启动页广告
使用下载链接中的后缀作为文件名进行存储,下载前根据名字判断启动页广告是否已经存在,优先使用缓存图片。
/**
* 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())//请求网络 在调度者的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);
/*使用工具类对图片进行处理*/
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 图片字节数组
* @param targetWidth 目标宽度
* @param targetHeight 目标高度
* @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) {
/*高度需要放大、宽度需要缩小才能铺满屏幕,主流机型不存在该情况,铺满为主要目标,继续放大*/
finalRatio = hRatio;
} else if (hRatio < 1f && wRatio >= 1.0f) {
/*高度需要缩小、宽度需要放大才能铺满屏幕,主流机型不存在该情况,铺满为主要目标,继续放大*/
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;
}
/**
* 裁剪头部区域
*
* @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);
}
/**
* 裁剪头部区域
*
* @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、高仿酷狗跳转控件(SkipView)
1、需求分析
1、圆形背景颜色自定义、半径自定义
2、倒计时时间自定义、圆环宽度自定义、圆环颜色自定义
3、文字大小、颜色和内容自定义
2、属性说明
属性名 | 默认值 | 备注 |
---|---|---|
progress_and_circle_distance | 10 | 圆环与圆形背景的距离 |
progress_and_text_distance | 年龄 | 圆环与文字的间距 |
progress_width | 6 | 圆环的宽度 |
text_size | 20 | 文字大小 |
progress_color | Color.WHITE | 倒计时圆环颜色 |
circle_color | Color.parseColor("#38342e") | 圆形背景颜色 |
text_color | Color.WHITE | 文本颜色 |
text | 跳过 | 文本内容 |
3、实现流程
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);
/*文字相关*/
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, "移除所有监听器: ");
mValueAnimator.removeAllListeners();
mValueAnimator.cancel();
mValueAnimator = null;
}
}
}
绘制过程中,有两点技巧需要牢记。
1、进度的值由-360~0进行变化
mValueAnimator.setFloatValues(-360, 0);
2、-90意思是在圆环的中心点的正上方开始绘制、mCurPro的意思是绘制的度数、正值顺时针绘制、负值逆时针绘制
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