Android自定义View实现扫描效果

本文实例为大家分享了Android自定义View实现扫描效果的具体代码,供大家参考,具体内容如下

演示效果如下:

Android自定义View实现扫描效果_第1张图片

实现内容:

1、控制动画是竖向或者横向
2、控制动画初始是从底部/左边开始,或者从上边/右边开始
3、控制动画的时常
4、可以自定义动画素材

具体实现:

自定义属性:


 
 
 
 

自定义 View:

实现原理,就是对 bitmap 做 Translate、preScale 转换,然后不断的 draw bitmap。其中 Translate 是为了实现位移效果,preScale 则是为了来回转换方向。

class ScanView : View {

 /**
  * 扫描的图片drawable
  */
 private var scanImg: Drawable? = null
 private lateinit var paint: Paint
 /**
  * 控件的宽
  */
 private var viewWidth: Int = 0
 /**
  * 控件的高
  */
 private var viewHeight: Int = 0
 private var bitmapMatrix = Matrix()
 
 private var scanBitmap: Bitmap? = null
 
 /**
  * 扫描图片需要显示的高度
  */
 private var showBitmapHeight: Float = 0F

 /**
  * 控制动画是竖向或者横向
  */
 private var isVertical = true
 /**
  * 控制动画初始是从底部/左边开始(true),或者从上边/右边开始(false)
  */
 private var isStartFromBottom = true

 private var isPositive = true

 fun setVertical(isVertical: Boolean) {
  this.isVertical = isVertical
  stopScanAnimAndReset()
  setScanBitmap()
 }

 fun setStartFromBootom(isFromBottom: Boolean) {
  this.isStartFromBottom = isFromBottom
  stopScanAnimAndReset()
 }

 /**
  * 属性动画
  */
 private var valueAnimator: ValueAnimator? = null

 /**
  * 动画时长
  */
 private var animDuration: Long = 1000L

 constructor(context: Context) : this(context, null)
 constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
 constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
  initAttribute(context, attrs)
  init()
 }

 private fun initAttribute(context: Context, attrs: AttributeSet?) {
  attrs?.let {
   val typedArray = context.obtainStyledAttributes(attrs, R.styleable.ScanView)
   scanImg = typedArray.getDrawable(R.styleable.ScanView_unit_scan_img)
   animDuration = typedArray.getInt(R.styleable.ScanView_anim_duration, 1000).toLong()
   typedArray.recycle()
  }
 }

 fun setAnimDuration(time: Long) {
  animDuration = time
 }

 private fun init() {
  paint = Paint(Paint.ANTI_ALIAS_FLAG)
  paint.style = Paint.Style.FILL
 }

 private fun getBitmapFromDrawable(drawable: Drawable): Bitmap? {
  var unitImgBitmap = drawable.toBitmap()

  if (unitImgBitmap.isRecycled) {
   return null
  }

  if (unitImgBitmap.isRecycled) {
   return null
  }

  // 处理横置的时候图片的旋转(因为视觉给的图一般是一个竖向的图,因此在横置的时候,手动将图片同步横置)
  if (!isVertical) {
   val matrix = Matrix()
   matrix.postRotate(90f)
   val resizedBitmap = Bitmap.createBitmap(unitImgBitmap, 0, 0,
     unitImgBitmap.width, unitImgBitmap.height, matrix, true)
   if (resizedBitmap != unitImgBitmap && unitImgBitmap != null && !unitImgBitmap.isRecycled) {
    unitImgBitmap.recycle()
    unitImgBitmap = resizedBitmap
   }
  }

  var realWidth: Int
  val finalBitmap: Bitmap
  val realUnitImgWidth: Float

  if (isVertical) {
   realWidth = viewWidth
   finalBitmap = Bitmap.createBitmap(realWidth, unitImgBitmap.height, Bitmap.Config.ARGB_8888)
   realUnitImgWidth = unitImgBitmap.width.toFloat()
  } else {
   realWidth = viewHeight
   finalBitmap = Bitmap.createBitmap(unitImgBitmap.width, realWidth, Bitmap.Config.ARGB_8888)
   realUnitImgWidth = unitImgBitmap.height.toFloat()
  }

  val canvas = Canvas(finalBitmap)
  // 向上取整
  val count = ceil(realWidth / realUnitImgWidth).toInt()

 // 为了解决适配问题,因为不同手机宽度不同,如果 UI 只提供了一个尺寸的素材,则可能会出现拉伸
 // 导致视觉效果不好的问题。这里换一种解决思路,即不将图片进行缩放,而是根据时机的宽度,
 // 去重复拼凑 unitImgBitmap,使其转换为一个充满宽度的整图,从而避免缩放导致的拉伸问题。
 // 需要注意的是,此时需要跟视觉协商,只需要给最小单元的图片素材即可。
  if (isVertical) {
   for (i in 0 until count) {
    canvas.drawBitmap(unitImgBitmap,i * realUnitImgWidth, 0f, paint)
   }
  } else {
   for (i in 0 until count) {
    canvas.drawBitmap(unitImgBitmap,0f, i * realUnitImgWidth, paint)
   }
  }

  return finalBitmap
 }

 override fun onDraw(canvas: Canvas) {
  super.onDraw(canvas)

  scanBitmap?.let {
   canvas.drawBitmap(it, bitmapMatrix, paint)
  }
 }

 /**
  * 开始做属性动画
  */
 fun startScanAnim() {
  valueAnimator?.takeIf { it.isRunning }?.let { it.cancel() }
  val value = if(isVertical) viewHeight.toFloat() else viewWidth.toFloat()
  valueAnimator = if (isStartFromBottom) {
   ValueAnimator.ofFloat(value + showBitmapHeight, -showBitmapHeight)
  } else {
   ValueAnimator.ofFloat(-showBitmapHeight, value + showBitmapHeight)
  }
  valueAnimator?.apply {
   // 使得扫描动画在横竖状态下都是相同的速度
   duration = if (isVertical) animDuration else (animDuration * 1.0f / viewWidth * viewHeight).toLong()
   repeatCount = -1
   repeatMode = ValueAnimator.REVERSE
   addUpdateListener(getUpdateListener())
   addListener(object : AnimatorListenerAdapter() {
    override fun onAnimationRepeat(animation: Animator) {
     // 用于控制 scan img 动画来回时的方向
     isPositive = !isPositive
    }
   })
   start()
  }
 }

 // 通过直接返回对应的 ValueAnimator.AnimatorUpdateListener,
 // 而不是在 ValueAnimator.AnimatorUpdateListener 回调中做 if 判断,提高性能
 private fun getUpdateListener(): ValueAnimator.AnimatorUpdateListener {
  return if (isVertical) {
   if (isStartFromBottom) {
    ValueAnimator.AnimatorUpdateListener { animation ->
     val value = animation.animatedValue as? Float ?: return@AnimatorUpdateListener
     bitmapMatrix.setTranslate(0F, value)
     // 使得 bitmap 来回动画的时候,方向是相对的
     bitmapMatrix.preScale(1.0f, if (isPositive) -1.0f else 1.0f)
     invalidate()
    }
   } else {
    ValueAnimator.AnimatorUpdateListener { animation ->
     val value = animation.animatedValue as? Float ?: return@AnimatorUpdateListener
     bitmapMatrix.setTranslate(0F, value)
     // 使得 bitmap 来回动画的时候,方向是相对的
     bitmapMatrix.preScale(1.0f, if (isPositive) 1.0f else -1.0f)
     invalidate()
    }
   }
  } else {
   if (isStartFromBottom) {
    ValueAnimator.AnimatorUpdateListener { animation ->
     val value = animation.animatedValue as? Float ?: return@AnimatorUpdateListener
     bitmapMatrix.setTranslate(value, 0f)
     bitmapMatrix.preScale(if (isPositive) 1.0f else -1.0f, 1.0f)
     invalidate()
    }
   } else {
    ValueAnimator.AnimatorUpdateListener { animation ->
     val value = animation.animatedValue as? Float ?: return@AnimatorUpdateListener
     bitmapMatrix.setTranslate(value, 0f)
     bitmapMatrix.preScale(if (isPositive) -1.0f else 1.0f, 1.0f)
     invalidate()
    }
   }
  }
 }

 /**
  * 停止属性动画
  */
 fun stopScanAnimAndReset() {
  valueAnimator?.takeIf { it.isRunning }?.cancel()
  reset(true)
 }

 /**
  * 重置为初始状态
  */
 private fun reset(isInvalidate: Boolean) {
  bitmapMatrix.reset()

  isPositive = true
  if (isVertical) {
   if (isStartFromBottom) {
    bitmapMatrix.setTranslate(0F, viewHeight + showBitmapHeight)
    bitmapMatrix.preScale(1.0f, if (isPositive) -1.0f else 1.0f)
   } else {
    bitmapMatrix.setTranslate(0F, -showBitmapHeight)
    bitmapMatrix.preScale(1.0f, if (isPositive) 1.0f else -1.0f)
   }
  } else {
   if (isStartFromBottom) {
    bitmapMatrix.setTranslate(viewWidth + showBitmapHeight, 0f)
    bitmapMatrix.preScale(if (isPositive) 1.0f else -1.0f, 1.0f)
   } else {
    bitmapMatrix.setTranslate(-showBitmapHeight, 0f)
    bitmapMatrix.preScale(if (isPositive) -1.0f else 1.0f, 1.0f)
   }
  }

  if (isInvalidate) {
   invalidate()
  }
 }

 private fun setScanBitmap() {
  if (scanImg == null || viewWidth <= 0 || viewHeight <= 0) {
   return
  }

  val bitmap = getBitmapFromDrawable(scanImg!!) ?: return
  scanBitmap = bitmap

  showBitmapHeight = if (isVertical) bitmap.height.toFloat() else bitmap.width.toFloat()

  reset(false)
 }

 override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
  super.onSizeChanged(w, h, oldw, oldh)
  viewWidth = w
  viewHeight = h
  setScanBitmap()
 }

 override fun onDetachedFromWindow() {
  super.onDetachedFromWindow()
  stopScanAnimAndReset()
 }
}

需要注意,上述代码的实现,有一个暂时还没有思路解决的问题,即比如动画从上扫描到底部,然后转换方向再从底部往上的时候,这里其实是有一段时间间隔的,并不是会马上从底部露出来。 只不过如果设置的动画时间比较短的话,感官上不会那么明显。

另外,上述代码中用到了系统提供的扩展方法 Drawable.toBitmap()

// androidx.core:core-ktx:1.3.2

fun Drawable.toBitmap(
 @Px width: Int = intrinsicWidth,
 @Px height: Int = intrinsicHeight,
 config: Config? = null
): Bitmap {
 if (this is BitmapDrawable) {
  if (config == null || bitmap.config == config) {
   // Fast-path to return original. Bitmap.createScaledBitmap will do this check, but it
   // involves allocation and two jumps into native code so we perform the check ourselves.
   if (width == intrinsicWidth && height == intrinsicHeight) {
    return bitmap
   }
   return Bitmap.createScaledBitmap(bitmap, width, height, true)
  }
 }

 val (oldLeft, oldTop, oldRight, oldBottom) = bounds

 val bitmap = Bitmap.createBitmap(width, height, config ?: Config.ARGB_8888)
 setBounds(0, 0, width, height)
 draw(Canvas(bitmap))

 setBounds(oldLeft, oldTop, oldRight, oldBottom)
 return bitmap
}

最后附带本身使用的动画图片素材(是一张矢量图):

Android自定义View实现扫描效果_第2张图片


 
 
  
  
  
  
  
 
 

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持脚本之家。

你可能感兴趣的:(Android自定义View实现扫描效果)