对QQ浏览器的广告图片切换挺感兴趣,最近忙里偷闲试验了一下,效果如图
大概效果就是这样了,上滑到一定程度的时候从图片右下角开始以圆形扩散切换至另一张图片,当图片切换完成后再下滑就会从图片左上角以圆形扩散的方式再切换回去; 如果图片没有切换完成就划回去,则圆形逐渐缩小至消失.
思路如上,两张图片,先显示第一张,滑动到一定程度后以圆形逐渐显示第二张图片.图片切换完全之后,再将两张图片的顺序互换重来一遍即可.如何显示圆形图片可以参考这里.
代码如下
自定义的view:
private val mPaint = Paint()//AnimView画笔
private val ovalPaint = Paint()//蒙版画笔
private val maskPaint = Paint()//圆形画笔
private val clearPaint = Paint()//重置画笔
private var topImg: Int = 0//第一张显示图片id
private var bottomImg: Int = 0//第二章显示图片id
private var topBitmap: Bitmap? = null//第一张显示图片bitmap
private var bottomBitmap: Bitmap? = null//第二张显示图片bitmap
private var tBitmap: Bitmap? = null//图片bitmap1
private var bBitmap: Bitmap? = null//图片bitmap2
private var bottomCanvasBitmap: Bitmap? = null//第二张显示图片bitmap底版
private var maskCanvasBitmap: Bitmap? = null//蒙版bitmap底版
private var mRatio = -1f
private var offset = 0.75f//第二张图片显示偏移量
private var radius = 0//圆形半径
private lateinit var bottomCanvas: Canvas
private lateinit var maskCanvas: Canvas
init {
/*
* 在xml中初始化并设置两张图片
*/
val typedArray: TypedArray? = context.obtainStyledAttributes(attrs, R.styleable.AnimView)
topImg = typedArray?.getResourceId(R.styleable.AnimView_topImg, 0) ?: 0
bottomImg = typedArray?.getResourceId(R.styleable.AnimView_bottomImg, 0) ?: 0
typedArray?.recycle()
tBitmap = BitmapFactory.decodeResource(resources, topImg)
bBitmap = BitmapFactory.decodeResource(resources, bottomImg)
maskPaint.color = Color.WHITE
ovalPaint.xfermode = PorterDuffXfermode(PorterDuff.Mode.DST_IN)
clearPaint.xfermode = PorterDuffXfermode(PorterDuff.Mode.CLEAR)
}
fun setImg(@DrawableRes top:Int,@DrawableRes bottom:Int){
tBitmap = BitmapFactory.decodeResource(resources, top)
bBitmap = BitmapFactory.decodeResource(resources, bottom)
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
if (topBitmap == null || bottomBitmap == null)
return
//先画第一张
canvas.drawBitmap(topBitmap, 0f, 0f, null)
/*
* 这个比率是在activity里根据scrolly算的,也可以改用anim写
*/
if (mRatio in 0f..0.8f) {
//先画出第二张图
bottomCanvas.drawBitmap(bottomBitmap, 0f, 0f, null)
//再画出圆形
maskCanvas.drawOval(measuredWidth * offset - radius * mRatio, measuredHeight * offset - radius * mRatio, measuredWidth * offset + radius * mRatio, measuredHeight * offset + radius * mRatio, maskPaint)
//在第二张图上采用DST_IN的模式叠加蒙版
bottomCanvas.drawBitmap(maskCanvasBitmap, 0f, 0f, ovalPaint)
//最后将合成好的图片画到自定义view里
canvas.drawBitmap(bottomCanvasBitmap, 0f, 0f, mPaint)
}
}
fun setRatio(ratio: Float) {
/*
* 当根据activity中的计算,当ratio等于0.75的时候,第二张图已经完全覆盖第一张图了
* 写0.8指只是为了留点偏差
* 这个时候就把两图互换
*/
if (ratio > 0.8f) {
topBitmap = bBitmap
bottomBitmap = tBitmap
//设置圆心为左上角
offset = 0.25f
}
/*
* 同上,当ratio为0时第二张图完全覆盖第一张
* 这里没有留偏差,因为不太好处理
* 这个时候再把两个图片换回来
*/
if (ratio < 0f) {
topBitmap = tBitmap
bottomBitmap = bBitmap
//设置圆心为右下角
offset = 0.75f
}
//反转比率,不然从上边往下拉的时候就是从大圆到小圆了
mRatio = if (offset == 0.25f) 0.8f - ratio else ratio
//清除之前的蒙版图
maskCanvas.drawPaint(clearPaint)
invalidate()
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
/*
* 对要用的图片做宽高处理,我是将第二张图片按第一张的宽高缩放的
*/
tBitmap = tBitmap?.scaleBitmap(measuredWidth, measuredHeight)
bBitmap = bBitmap?.scaleBitmap(measuredWidth, measuredHeight)
topBitmap = tBitmap
bottomBitmap = bBitmap
bottomCanvasBitmap = Bitmap.createBitmap(bottomBitmap?.width ?: measuredWidth, bottomBitmap?.height ?: measuredHeight, Bitmap.Config.ARGB_4444)
maskCanvasBitmap = Bitmap.createBitmap(bottomBitmap?.width ?: measuredWidth, bottomBitmap?.height ?: measuredHeight, Bitmap.Config.ARGB_4444)
bottomCanvas = Canvas(bottomCanvasBitmap)
maskCanvas = Canvas(maskCanvasBitmap)
radius = (measuredHeight / Math.sin(Math.atan(measuredHeight.toDouble() / measuredWidth.toDouble()))).toInt()
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
val wSize = MeasureSpec.getSize(widthMeasureSpec)
val hMode = MeasureSpec.getMode(heightMeasureSpec)
var hSize = MeasureSpec.getSize(heightMeasureSpec)
//默认是横向充满,纵向按照第一张图片的宽高比设置
if (hMode == MeasureSpec.AT_MOST || hMode == MeasureSpec.UNSPECIFIED) {
tBitmap?.let {
hSize = it.height * wSize / it.width
}
}
setMeasuredDimension(wSize, hSize)
}
}
activity中主要就是给scrollview一个滑动监听
mBinding.nsv.setOnScrollChangeListener { v: NestedScrollView, scrollX: Int, scrollY: Int, oldScrollX: Int, oldScrollY: Int ->
/*
* 按照基础高度处于屏幕的中间为准
* 在没有任何偏移的情况下 top-scrolly/base 就是ratio,并且在基础高度的底部时ratio为0.75,在顶部时为0
* 但是这样的话园就会由大到小了,所以要倒过来,用 (base - top + scrolly)/base,这样就是从0到0.75了
* 因为设置了300的高度偏移,为了使高度居中,所以要(base - top + scrolly+150)/base
* 因为第二张图片是从0.25宽高的位置开始覆盖,如果不乘以0.125,图片会在0.75*base处覆盖完,为了对称,所以...
*/
val base = mBinding.nsv.measuredHeight - mBinding.av.measuredHeight - 300f
mBinding.av.setRatio((base - 1.125f * mBinding.av.top + 1.125f * scrollY + 150f) / base)
}
xml:
如有问题或不足之处,欢迎批评指正