Android实现雷达View效果的示例代码

样式效果

还是先来看效果:

这是一个仿雷达扫描的效果,是之前在做地图sdk接入时就想实现的效果,但之前由于赶着毕业设计,就没有亲手去实现,不过现在自己撸一个发现还是挺简单的。

这里主要分享一下我的做法。

目录

主体轮廓的实现(雷达的结构)

动画的实现(雷达扫描的效果)

目标点的加入(图片/点)

主体轮廓实现

不难分析得出,这个View主要由外部的一个圆,中间的锚点圆以及扇形旋转区域组成。而且每个部分理应由不同的Paint去绘制,以方便去定制各部分的样式。

外部圆以及锚点圆的绘制较为简单,主要的点还是要对整个View的宽高进行一定的限制,例如宽高必须相等且在某种模式下,取小的那个值来限定整个RadarView的最大值。那么该如何去控制呢?

onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int)

  由于我们继承自View,在onMeasure方法中,我们可以根据两个参数来获取Mode,并且根据Mode来指定宽/高对应的值,再通过setMeasuredDimension去指定控件主体的宽高即可。

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
 super.onMeasure(widthMeasureSpec, heightMeasureSpec)
 val vWidth = measureDimension(widthMeasureSpec)
 val vHeight = measureDimension(heightMeasureSpec)
 val size = min(vWidth, vHeight)

 setMeasuredDimension(size, size)
}

private fun measureDimension(spec: Int) = when (MeasureSpec.getMode(spec)) {
 MeasureSpec.EXACTLY -> {
 // exactly number or match_parent
 MeasureSpec.getSize(spec)
 }
 MeasureSpec.AT_MOST -> {
 // wrap_content
 min(mDefaultSize, MeasureSpec.getSize(spec))
 }
 else -> {
 mDefaultSize
 }
}

测量工作完成了,我们自然可以去绘制了。为了不让中间的小圆看起来那么突兀(偏大或偏小),这里设置了一个scaleFactor的缩放因子,使其能根据外圆的尺寸来进行缩放。

override fun onDraw(canvas: Canvas?) {
 super.onDraw(canvas)
 // draw outside circle (background)
 canvas?.drawCircle(measuredWidth.toFloat() / 2, measuredHeight.toFloat() / 2, measuredWidth.toFloat() / 2, mOutlinePaint)
 if (mBorderWidth > 0F && mOutlinePaint.shader == null) {
  drawBorder(canvas)
 }

 // mOutlineRect = Rect(0, 0, measuredWidth, measuredHeight)
 canvas?.drawArc(mOutlineRect.toRectF(), mStartAngle, mSweepAngle, true, mSweepPaint)

 // draw center circle
 // scaleFactor = 30F
 canvas?.drawCircle(measuredWidth.toFloat() / 2, measuredHeight.toFloat() / 2, measuredWidth.toFloat() / 2 / mScaleFactor, mPaint)
}

private fun drawBorder(canvas: Canvas?) {
 Log.i("RadarView", "drawBorder")
 mOutlinePaint.style = Paint.Style.STROKE
 mOutlinePaint.color = mBorderColor
 mOutlinePaint.strokeWidth = mBorderWidth
 canvas?.drawCircle(measuredWidth.toFloat() / 2, measuredHeight.toFloat() / 2,
  (measuredWidth.toFloat() - mBorderWidth) / 2, mOutlinePaint)
 // 还原
 mOutlinePaint.style = Paint.Style.FILL_AND_STROKE
 mOutlinePaint.color = mBackgroundColor
}

绘制了基准圆以后,要实现雷达扫描时那种渐变的效果,我们可以通过SweepGradient来操作。通过指定中心点,渐变颜色,以及颜色的分布,来定制扫描渐变的样式,默认的即时开头时gif展示的那种。由于这里是从第一象限开始旋转,因此将旋转的起点通过matrix逆时针旋转90度,从而达到由浅入深的效果。

private fun setShader(size: Int) {
 val shader = SweepGradient(size.toFloat() / 2, size.toFloat() / 2,
  mScanColors?: mDefaultScanColors, // 可通过setScanColors()来定制颜色
  floatArrayOf(0F, 0.5F, 1F)) // 这里默认走平均分布
 val matrix = Matrix()
 // 逆时针旋转90度
 matrix.setRotate(-90F, size.toFloat() / 2, size.toFloat() / 2)
 shader.setLocalMatrix(matrix)
 mSweepPaint.shader = shader
}

这里完成了测量与绘制的工作,那么我们在布局里引用以后,就会看到这样的效果:

这时,由于我们之前在测量的时候,将宽高最小值作为绘制的基准大小给予了RadarView,因此measuredWidth和measuredHeight是相等的,但是由于在布局中指定了match_parent属性,那么实际的控件宽高还是和父布局一致(在这里即占满屏幕宽高,由于宽比高小,所以看到绘制的图形会偏向上方;如果设置了高比宽小,那么绘制的图形就会位于左侧)。一般的雷达控件应该都是居中显示的,所以我在这里也重写了onLayout方法,来实现居中的效果。

override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
 // 设置默认居中
 var l = left
 var r = right
 var t = top
 var b = bottom
 when {
  width > height -> {
   // 宽度比高度大 那么要设置默认居中就得把left往右移 right往左移
   l = (width - measuredWidth) / 2
   r = width - l
   layout(l, t, r, b)
  }
  height > width -> {
   // 高度比宽度大 那么要设置默认居中就得把top往下移 bottom往上移
   t = (height - measuredHeight) / 2
   b = height - t
   layout(l, t, r, b)
  }
  else -> super.onLayout(changed, left, top, right, bottom)
 }
}

动画的实现

完成了绘制,接下来就是思考该如何让他动起来了。由绘制的代码不难想到,我这里考虑的是通过mStartAngle的变化来控制绘制的角度旋转,而ValueAnimator则正好能获取到每次更新时value的值,因此这里我选用了这个方案。

fun start() {
 Log.i("RadarView", "animation start")
 mIsAnimating = true
 mAnimator.duration = 2000
 mAnimator.repeatCount = ValueAnimator.INFINITE
 mAnimator.addUpdateListener {
  val angle = it.animatedValue as Float
  mStartAngle = angle

//  Log.i("RadarView", "mStartAngle = $mStartAngle and curValue = ${it.animatedValue}")
  postInvalidate()
 }
 mAnimator.start()
}

这里就需要注意一个点,就是canvas在绘制时,后绘制的会覆盖在前绘制的图像上,所以需要注意绘制的顺序。当然,这里也可以把mOutlineRect的宽高设置为measuredWidth - mBorderWidth,那么就能保证绘制填充角度时,不会把边界覆盖。

至此,动画的效果便完成了。

目标点的加入

首先,前两点已经能满足大多的雷达扫描需求了。这里这个添加目标点(target)纯粹是我自己想加入的功能,因为觉得可以结合地图sdk的MapView来共同使用,目前也只是开发阶段,扩展性可能考虑得还不是特别充足,也还没应用到具体项目中。但是,总觉得自己想的功能也该试着去实践一下~

这里主要运用的圆的计算公式:

由于Android的坐标系的原点是在左上角,y轴过顶点向下延伸。由我们的绘制可知,此绘制图像在坐标系中的位置大概如下图所示:

那么,对应的公式就为:

要注意的是,这里r的计算会根据图/点的设置来动态计算,具体例子通过代码来进行分析。

// 随机落点
fun addTarget(size: Int, type: TYPE = TYPE.RANDOM) {
 val list = ArrayList<PointF>()
 val r = measuredWidth.toFloat() / 2
 val innerRect = Rect((r - r / mScaleFactor).toInt(), (r - r / mScaleFactor).toInt(),
  (r + r / mScaleFactor).toInt(), (r + r / mScaleFactor).toInt())
 // 圆的中心点
 val circle = PointF(measuredWidth.toFloat() / 2, measuredHeight.toFloat() / 2)
 while (list.size < size) {
  val ranX = Random.nextDouble(0.0, r * 2.0).toFloat()
  val ranY = Random.nextDouble(0.0, r * 2.0).toFloat()
  val ranPointF = PointF(ranX, ranY)
  if (innerRect.contains(ranPointF.toPoint())) {
   continue
  }
  // 圆公式
  if (!mNeedBitmap &&
   (ranX - circle.x).pow(2) + (ranY - circle.y).pow(2) <
    (r - mTargetRadius - mBorderWidth).toDouble().pow(2.0)) {
   // 普通点
   addTargetFromType(type, list, ranX, ranY, r, ranPointF)
  } else if (mNeedBitmap &&
   (ranX - circle.x).pow(2) + (ranY - circle.y).pow(2) <
    (r - mBorderWidth - max(mBitmap.width, mBitmap.height) / 2).toDouble().pow(2)) {
   // 图
   addTargetFromType(type, list, ranX, ranY, r, ranPointF)
  } else {
   continue
  }
 }
 mTargetList = list
 for (target in list) {
  Log.i("RadarView", "target = [${target.x}, ${target.y}]")
 }
 invalidate()
}

可以看到,当target为普通点时,r的计算还要减去targetRadius,即目标点的半径,同时还要减去边界的宽度,如图所示:

当target为图时,由于宽高不定,故除了边界外,还要减去大的边,那么r的计算则为:

同时为了避免图片的尺寸过大,这里同样采取了一个默认值与一个缩放因子,从而保证图的完整性以及避免过大而引起的视觉丑化。

关于落点的位置,目前采取的是随机落点,如果应用到地图扫点的话,可以通过地图sdk内的距离计算工具再与RadarView的坐标做一个比例转换,从而达到雷达内显示该点具体方位。

关于落点的分布,目前提供了5种类型:分别是全象限随机、第一象限、第二象限、第三象限与第四象限随机。

Github

若须直接调用,可移步至 https://github.com/CarsonWoo/RadarView

完整代码

class RadarView : View {

 enum class TYPE { RANDOM, FIRST, SECOND, THIRD, FOURTH }

 private val mPaint by lazy { Paint(Paint.ANTI_ALIAS_FLAG) }

 private val mSweepPaint by lazy { Paint(Paint.ANTI_ALIAS_FLAG) }

 private val mOutlinePaint by lazy { Paint(Paint.ANTI_ALIAS_FLAG) }

 private val mTargetPaint by lazy { Paint(Paint.ANTI_ALIAS_FLAG) }

 private val mDefaultSize = 120// px

 // limit the size of bitmap
 private var mBitmapMaxSize = 0F

 private var mBitmapWHRatio = 0F

 private val mScaleFactor = 30F

 private var mStartAngle = 0F
 private val mSweepAngle = -60F

 private var mScanColors: IntArray? = null

 private val mDefaultScanColors = intArrayOf(Color.parseColor("#0F7F7F7F"),
  Color.parseColor("#7F7F7F7F"),
  Color.parseColor("#857F7F7F"))

 private val mDefaultBackgroundColor = Color.WHITE

 private var mBackgroundColor: Int = mDefaultBackgroundColor

 private var mBorderColor: Int = Color.BLACK

 private var mBorderWidth = 0F

 private var mTargetColor: Int = Color.RED

 private var mTargetRadius = 10F

 private lateinit var mOutlineRect: Rect

 private val mAnimator = ValueAnimator.ofFloat(0F, 360F)

 private var mTargetList: ArrayList<PointF>? = null

 private var mIsAnimating = false

 private var mNeedBitmap = false

 private var mBitmap = BitmapFactory.decodeResource(resources, R.mipmap.ic_launcher)

 constructor(context: Context): this(context, null)

 constructor(context: Context, attributeSet: AttributeSet?) : super(context, attributeSet)

 init {
  mPaint.color = Color.GRAY
  mPaint.strokeWidth = 10F
  mPaint.style = Paint.Style.FILL_AND_STROKE
  mPaint.strokeJoin = Paint.Join.ROUND
  mPaint.strokeCap = Paint.Cap.ROUND

  mSweepPaint.style = Paint.Style.FILL

  mOutlinePaint.style = Paint.Style.FILL_AND_STROKE
  mOutlinePaint.color = mBackgroundColor

  mTargetPaint.style = Paint.Style.FILL
  mTargetPaint.color = mTargetColor
  mTargetPaint.strokeWidth = 10F
 }

 override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
  super.onMeasure(widthMeasureSpec, heightMeasureSpec)
  val vWidth = measureDimension(widthMeasureSpec)
  val vHeight = measureDimension(heightMeasureSpec)
  val size = min(vWidth, vHeight)

  setShader(size)

  setMeasuredDimension(size, size)

  setParamUpdate()
 }

 override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
  // 设置默认居中
  var l = left
  var r = right
  var t = top
  var b = bottom
  when {
   width > height -> {
    // 宽度比高度大 那么要设置默认居中就得把left往右移 right往左移
    l = (width - measuredWidth) / 2
    r = width - l
    layout(l, t, r, b)
   }
   height > width -> {
    // 高度比宽度大 那么要设置默认居中就得把top往下移 bottom往上移
    t = (height - measuredHeight) / 2
    b = height - t
    layout(l, t, r, b)
   }
   else -> super.onLayout(changed, left, top, right, bottom)
  }
 }

 private fun setShader(size: Int) {
  val shader = SweepGradient(size.toFloat() / 2, size.toFloat() / 2,
   mScanColors?: mDefaultScanColors,
   floatArrayOf(0F, 0.5F, 1F))
  val matrix = Matrix()
  matrix.setRotate(-90F, size.toFloat() / 2, size.toFloat() / 2)
  shader.setLocalMatrix(matrix)
  mSweepPaint.shader = shader
 }

 fun setScanColors(colors: IntArray) {
  this.mScanColors = colors
  setShader(measuredWidth)
  invalidate()
 }

 fun setRadarColor(@ColorInt color: Int) {
  this.mBackgroundColor = color
  this.mOutlinePaint.color = color
  invalidate()
 }

 fun setRadarColor(colorString: String) {
  if (!colorString.startsWith("#") || colorString.length != 7 || colorString.length != 9) {
   Log.e("RadarView", "colorString parse error, please check your enter param")
   return
  }
  val color = Color.parseColor(colorString)
  setRadarColor(color)
 }

 fun setBorderColor(@ColorInt color: Int) {
  this.mBorderColor = color
  invalidate()
 }

 fun setBorderColor(colorString: String) {
  if (!colorString.startsWith("#") || colorString.length != 7 || colorString.length != 9) {
   Log.e("RadarView", "colorString parse error, please check your enter param")
   return
  }
  val color = Color.parseColor(colorString)
  setBorderColor(color)
 }

 fun setRadarGradientColor(colors: IntArray) {
  val shader = SweepGradient(measuredWidth.toFloat() / 2,
   measuredHeight.toFloat() / 2, colors, null)
  mOutlinePaint.shader = shader
  invalidate()
 }

 fun setBorderWidth(width: Float) {
  this.mBorderWidth = width
  invalidate()
 }

 private fun setParamUpdate() {
  mOutlineRect = Rect(0, 0, measuredWidth, measuredHeight)

  mBitmapMaxSize = measuredWidth.toFloat() / mScaleFactor
 }

 private fun measureDimension(spec: Int) = when (MeasureSpec.getMode(spec)) {
  MeasureSpec.EXACTLY -> {
   // exactly number or match_parent
   MeasureSpec.getSize(spec)
  }
  MeasureSpec.AT_MOST -> {
   // wrap_content
   min(mDefaultSize, MeasureSpec.getSize(spec))
  }
  else -> {
   mDefaultSize
  }
 }

 override fun setBackground(background: Drawable?) {
  // 取消传统背景设置
//  super.setBackground(background)
 }

 override fun onDraw(canvas: Canvas?) {
  super.onDraw(canvas)
  // draw outside circle (background)
  canvas?.drawCircle(measuredWidth.toFloat() / 2, measuredHeight.toFloat() / 2, measuredWidth.toFloat() / 2, mOutlinePaint)
  if (mBorderWidth > 0F && mOutlinePaint.shader == null) {
   drawBorder(canvas)
  }

  canvas?.drawArc(mOutlineRect.toRectF(), mStartAngle, mSweepAngle, true, mSweepPaint)

  if (!mTargetList.isNullOrEmpty() && !mIsAnimating) {
   drawTarget(canvas)
  }

  // draw center circle
  canvas?.drawCircle(measuredWidth.toFloat() / 2, measuredHeight.toFloat() / 2, measuredWidth.toFloat() / 2 / mScaleFactor, mPaint)
 }

 private fun drawBorder(canvas: Canvas?) {
  Log.i("RadarView", "drawBorder")
  mOutlinePaint.style = Paint.Style.STROKE
  mOutlinePaint.color = mBorderColor
  mOutlinePaint.strokeWidth = mBorderWidth
  canvas?.drawCircle(measuredWidth.toFloat() / 2, measuredHeight.toFloat() / 2,
   (measuredWidth.toFloat() - mBorderWidth) / 2, mOutlinePaint)
  // 还原
  mOutlinePaint.style = Paint.Style.FILL_AND_STROKE
  mOutlinePaint.color = mBackgroundColor
 }

 private fun drawTarget(canvas: Canvas?) {
  mTargetList?.let {
   Log.e("RadarView", "draw target")
   for (target in it) {
    if (mNeedBitmap) {
     canvas?.drawBitmap(mBitmap, target.x - mBitmap.width / 2,
      target.y - mBitmap.height / 2, mTargetPaint)
    } else {
     canvas?.drawCircle(target.x, target.y, mTargetRadius, mTargetPaint)
    }
   }
  }
 }

 fun setBitmapEnabled(enabled: Boolean, drawable: Drawable) {
  // 这里是为了防止界面还未获取到宽高时 会导致onMeasure走不到 那么maxSize就会为0
  post {
   this.mNeedBitmap = enabled
   this.mBitmapWHRatio = drawable.intrinsicWidth.toFloat() / drawable.intrinsicHeight.toFloat()
   mBitmap = if (mBitmapWHRatio >= 1) {
    // 宽比高大
    drawable.toBitmap(
     width = min(mBitmapMaxSize, drawable.intrinsicWidth.toFloat()).toInt(),
     height = (min(mBitmapMaxSize, drawable.intrinsicWidth.toFloat()) / mBitmapWHRatio).toInt(),
     config = Bitmap.Config.ARGB_8888)
   } else {
    // 高比宽大
    drawable.toBitmap(
     height = min(mBitmapMaxSize, drawable.intrinsicHeight.toFloat()).toInt(),
     width = (min(mBitmapMaxSize, drawable.intrinsicHeight.toFloat()) * mBitmapWHRatio).toInt(),
     config = Bitmap.Config.ARGB_8888
    )
   }
  }
 }

 // 随机落点
 fun addTarget(size: Int, type: TYPE = TYPE.RANDOM) {
  val list = ArrayList<PointF>()
  val r = measuredWidth.toFloat() / 2
  val innerRect = Rect((r - r / mScaleFactor).toInt(), (r - r / mScaleFactor).toInt(),
   (r + r / mScaleFactor).toInt(), (r + r / mScaleFactor).toInt())
  // 圆的中心点
  val circle = PointF(measuredWidth.toFloat() / 2, measuredHeight.toFloat() / 2)
  while (list.size < size) {
   val ranX = Random.nextDouble(0.0, r * 2.0).toFloat()
   val ranY = Random.nextDouble(0.0, r * 2.0).toFloat()
   val ranPointF = PointF(ranX, ranY)
   if (innerRect.contains(ranPointF.toPoint())) {
    continue
   }
   // 圆公式
   if (!mNeedBitmap &&
    (ranX - circle.x).pow(2) + (ranY - circle.y).pow(2) <
     (r - mTargetRadius - mBorderWidth).toDouble().pow(2.0)) {
    // 在圆内
    addTargetFromType(type, list, ranX, ranY, r, ranPointF)
   } else if (mNeedBitmap &&
    (ranX - circle.x).pow(2) + (ranY - circle.y).pow(2) <
     (r - mBorderWidth - max(mBitmap.width, mBitmap.height) / 2).toDouble().pow(2)) {
    addTargetFromType(type, list, ranX, ranY, r, ranPointF)
   } else {
    continue
   }
  }
  mTargetList = list
  for (target in list) {
   Log.i("RadarView", "target = [${target.x}, ${target.y}]")
  }
  invalidate()
 }

 private fun addTargetFromType(type: TYPE, list: ArrayList<PointF>, ranX: Float, ranY: Float,
         r: Float, ranPointF: PointF) {
  when (type) {
   TYPE.RANDOM -> {
    list.add(ranPointF)
   }
   TYPE.FOURTH -> {
    if (ranX in r.toDouble()..2 * r.toDouble() && ranY in r.toDouble()..2 * r.toDouble()) {
     list.add(ranPointF)
    }
   }
   TYPE.THIRD -> {
    if (ranX in 0.0..r.toDouble() && ranY in r.toDouble()..2 * r.toDouble()) {
     list.add(ranPointF)
    }
   }
   TYPE.SECOND -> {
    if (ranX in 0.0..r.toDouble() && ranY in 0.0..r.toDouble()) {
     list.add(ranPointF)
    }
   }
   TYPE.FIRST -> {
    if (ranX in r.toDouble()..2 * r.toDouble() && ranY in 0.0..r.toDouble()) {
     list.add(ranPointF)
    }
   }
  }
 }

 fun start() {
  Log.i("RadarView", "animation start")
  mIsAnimating = true
  mAnimator.duration = 2000
  mAnimator.repeatCount = ValueAnimator.INFINITE
  mAnimator.addUpdateListener {
   val angle = it.animatedValue as Float
   mStartAngle = angle

   Log.i("RadarView", "mStartAngle = $mStartAngle and curValue = ${it.animatedValue}")
   postInvalidate()
  }
  mAnimator.start()
 }

 fun start(startVal: Float, endVal: Float) {
  mIsAnimating = true
  mAnimator.setFloatValues(startVal, endVal)
  mAnimator.duration = 2000
  mAnimator.repeatCount = ValueAnimator.INFINITE
  mAnimator.addUpdateListener {
   mStartAngle = it.animatedValue as Float

   Log.i("RadarView", "mStartAngle = $mStartAngle and curValue = ${it.animatedValue}")
   postInvalidate()
  }
  mAnimator.start()
 }

 fun stop() {
  mIsAnimating = false
  if (mAnimator.isRunning) {
   mAnimator.cancel()
   mAnimator.removeAllListeners()
  }
  mStartAngle = 0F
 }

}

调用方式

override fun onCreate(savedInstanceState: Bundle?) {
 super.onCreate(savedInstanceState)
 setContentView(R.layout.activity_main)

 radar_view.setBorderWidth(5F)
 radar_view.setRadarColor(Color.TRANSPARENT)
 radar_view.setBitmapEnabled(true, resources.getDrawable(R.mipmap.ic_launcher_round))
//  radar_view.setScanColors(intArrayOf(Color.RED, Color.LTGRAY, Color.CYAN))
//  radar_view.setRadarGradientColor(intArrayOf(Color.RED, Color.GREEN, Color.BLUE))

 btn_start.setOnClickListener {
  radar_view.start()
//  workThreadAndCallback()
 }

 btn_stop.setOnClickListener {
  radar_view.stop()
  radar_view.addTarget(7)
 }
}

总结

到此这篇关于Android实现雷达View效果的文章就介绍到这了,更多相关android 雷达View效果内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • Android编程简单实现雷达扫描效果

    本文实例讲述了Android编程简单实现雷达扫描效果.分享给大家供大家参考,具体如下: 在eoe看到有一篇关于雷达扫描的文章,然后看了下,很简单,但是觉得还有很多可以优化的地方,下面贴出来 package com.example.wave; import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Matrix; im

  • Android动画之雷达扫描效果

    我们首先看一下效果图,有个整体的印象 好了,为了便于理解,这里就按照动画所见内容依次展开来说 准备 这里决定采用canvas(画布)和paint(画笔)实现了这个简单动画控件. 由图片可以看到有两条交叉的十字线.几个圆圈和一些白点,那么首先定义一下所需的画笔,画布及一些数据 setBackgroundColor(Color.TRANSPARENT); //宽度=5,抗锯齿,描边效果的白色画笔 mPaintLine = new Paint(); mPaintLine.setStrokeWidth(

  • Android仿微信雷达辐射搜索好友(逻辑清晰实现简单)

    不知不觉这个春节也已经过完了,遗憾家里没网,没能及时给大家送上祝福,今天回到深圳,明天就要上班了,小伙伴们是不是和我一样呢?今天讲的是一个大家都见过的动画,雷达搜索好友嘛,原理也十分的简单,你看完我的分析,也会觉得很简单了,国际惯例,无图无真相,我们先看看效果图,对了,真 测试机送人了,所讲这段时间应该一直用模拟器显示吧! 仿微信雷达扫描,仿安卓微信.云播雷达扫描动画效果点击中间的黑色圆圈开始扫描动画,再次点击复位,需要这种效果的朋友可以自己下载看一下. 效果图如下所示: 这个界面相信大家都认识

  • Android自定义ViewGroup实现绚丽的仿支付宝咻一咻雷达脉冲效果

    去年春节的时候支付宝推行的集福娃活动着实火的不能再火了,更给力的是春晚又可以全民参与咻一咻集福娃活动,集齐五福就可平分亿元大红包,只可惜没有敬业福--那时候在家没事写了个咻一咻插件,只要到了咻一咻的时间点插件就可以自动的点击咻一咻来咻红包,当时只是纯粹练习这部分技术代码没有公开,后续计划写篇关于插件这方面的文章,扯远了(*^__^*) --我们知道在支付宝的咻一咻页面有个雷达扩散的动画效果,当时感觉动画效果非常棒,于是私下尝试着实现了类似的效果,后来在github发现有大神也写有类似效果,于是读

  • Android RadarView雷达图(蜘蛛网图)的实现代码

    公司产品需要一个雷达图来展示各维度的比重,网上找了一波,学到不少,直接自己上手来撸一记 无图言虚空 简单分析一波,确定雷达图正几边形的--正五边形 int count=5,分为几个层数--4 层 int layerCount=4 @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); drawPolygon(canvas);//画边 drawLines(canvas);//画线 drawText(canvas)

  • Android仿微信、QQ附近好友雷达扫描效果

    1.概述 最近一直到在带实习生,因为人比较多,所以很长一段时间没有更新博客了,今天更新一篇雷达扫描附近好友效果,以后尽量每周更新一篇,先看一下效果: 2.实现 1.效果分析 效果分为两个部分,一个是上半部分的自定义RadarView,还有就是下半部分的ViewPager,至于怎么做到缩放和背景虚化的效果大家可以去看看LazyViewPager这里不详细介绍,这里主要实现扫描效果部分. 2.扫描效果实现 2.1自定义RadarView在onDraw()方法中画六个圆圈,至于圆圈的半径是多少我们需要

  • Android实现雷达View效果的示例代码

    样式效果 还是先来看效果: 这是一个仿雷达扫描的效果,是之前在做地图sdk接入时就想实现的效果,但之前由于赶着毕业设计,就没有亲手去实现,不过现在自己撸一个发现还是挺简单的. 这里主要分享一下我的做法. 目录 主体轮廓的实现(雷达的结构) 动画的实现(雷达扫描的效果) 目标点的加入(图片/点) 主体轮廓实现 不难分析得出,这个View主要由外部的一个圆,中间的锚点圆以及扇形旋转区域组成.而且每个部分理应由不同的Paint去绘制,以方便去定制各部分的样式. 外部圆以及锚点圆的绘制较为简单,主要的点

  • Android输入框实时模糊搜索效果的示例代码

    Android输入框实时模糊搜索 很多开发场景会用到搜索框实时模糊搜索来帮助用户输入内容,如图 思路是在EditText 字符变动的时候 弹出ListPopupwindow并更新列表,这样的做法google已经封装为AutoCompleteTextView 用法 mAutoCompleteTextView.setAdapter(adapter); mAutoCompleteTextView.setFocusable(true); mAutoCompleteTextView.setOnItemCl

  • android实现音乐跳动效果的示例代码

    效果图 实现 整体的流程图如下 上面主要步骤分为3个 1.计算宽度能放下多少列的音频块. 2.计算每一列中音频块的个数 3.绘制音频块 1.计算宽度能放下多少列的音频块. 设置音频块的宽度为danceWidth,音频块横向之间的间距为danceGap,那么可以算出能放的列数: /** * 先计算当前宽度能够放下多少个音频块 */ val widthNum = (getAvailableWith() / (danceGap + danceWidth)).toInt() /** * 获取可以用的宽度

  • Android Flutter实现点赞效果的示例代码

    目录 前言 绘制小手 完整源码 前言 点赞这个动作不得不说在社交.短视频等App中实在是太常见了,当用户手指按下去的那一刻,给用户一个好的反馈效果也是非常重要的,这样用户点起赞来才会有一种强烈的我点了赞的效果,那么今天我们就用Flutter实现一个掘金App上的点赞效果. 首先我们看下掘金App的点赞组成部分,有一个小手,点赞数字.点赞气泡效果,还有一个震动反馈,接下来我们一步一步实现. 知识点:绘制.动画.震动反馈 绘制小手 这里我们使用Flutter的Icon图标中的点赞小手,Icons图标

  • Android 自定义加载动画Dialog弹窗效果的示例代码

    效果图 首先是创建弹窗的背景 这是上面用到的 以shape_bg_5_blue.xml为例,其他的三个无非就是里面的颜色不一样而已 <?xml version="1.0" encoding="utf-8"?> <shape xmlns:android="http://schemas.android.com/apk/res/android"> <corners android:radius="5dp"

  • Android仿高德首页三段式滑动效果的示例代码

    目录 高德的效果 实现的效果 自定义View源码 xml布局中的使用 高德首页按钮处理 源码地址 最近发现很多app都使用了三段式滑动,比如说高德的首页和某宝等物流信息都是使用的三段式滑动方式,谷歌其实给了我们很好的2段式滑动,就是BottomSheet,所以这次我也是在这个原理基础上做了一个小小的修改来实现我们今天想要的效果. 高德的效果 实现的效果 我们实现的效果和高德差距不是很大,也很顺滑.具体实现其实就是继承CoordinatorLayout.Behavior 自定义View源码 /**

  • Android实现随意拖动View效果的实例代码

    项目过程中要实现能在页面中随意的拖动,刚开始实现是用悬浮球的形式进行实现,因为之前项目中用过,实现后发现用户每次安装后,都有权限的限制,甚至有些用户关闭悬浮球权限之后,不知道怎么在手机上打开悬浮球的权限,这样的话用户体验很不好,所以自己重新自定义实现在页面中拖动,不需要请求权限. 自定义随意拖动View: package com.dragdemo; import android.annotation.SuppressLint; import android.content.Context; im

  • android 自定义圆角button效果的实例代码(自定义view Demo)

    概述 在平时开发过程中经常会碰到需要使用圆角button的情况,一般也会包括很多其他小功能,比如要在里面添加img,设置不同的圆角大小等. 针对这样的场景,直接使用创建多个shape,定义多个xml文件也是可以实现的.但是如果使用非常频繁,那么直接自定义一个就会来的非常方便. 甚至在一些情况下,不是可以用shape定义的规则图形,比如需要用到贝塞尔曲线等. 如果全局需要这样风格的view,那么自定义一个View是非常必要的. 本文主要是个demo记录,如有需要的读者可以借鉴学习. Demo 主要

  • Android自定义滑动验证条的示例代码

    本文介绍了Android自定义滑动验证条的示例代码,分享给大家,具体如下: *注:不知道为什么,h5的标签在这里没用了,所以我也只能用Markdown的语法来写了 项目地址:https://github.com/994866755/handsomeYe.seekbar.github.io 需求: 在我们的某些应用中需要滑动验证.比如说这个样子的: 刚开始我也很懵逼要怎么去弄,结果我去看了一些人的代码,有人是用自定义viewgroup去做,就是viewgroup包含滑动块和滑动条.但我觉得太麻烦,

  • Android 给图片加上水印的示例代码(支持logo+文字)

    本文介绍了Android 给图片加上水印的示例代码(支持logo+文字),分享给大家,具体如下: 现在我们想要往图片上打上水印,该水印应符合这样的需求的: 支持logo+文字: 文字信息支持多行展示: 用户可以选择水印在图片上的生成位置(左上.右上.右下和左下). 粗略的结构图低配版大概就长这样... 水印结构图.png 现在提供这样的一种思路去实现这一个需求,我们可以通过自定义一个view,view的布局中包含logo.公司名称和相关信息,这个view就是我们要打上图片的水印. 这样的一个vi

随机推荐