Android-高级-UI-进阶之路-(七)-SVG-基础使用-+-绘制中国地图

iv.setImageDrawable(animatedVectorDrawable)
val animatable = iv.drawable as Animatable
animatable.start()
}
}

实战

输入搜索动画

  1. 利用在线绘制 SVG 图标网站 制作搜索图标

可以自己随意捣鼓绘制,绘制好了之后点击视图->源代码,将 SVG 代码复制出来保存成 search_svg.xml

  1. 在线转换 svg2vector

点击空白或者直接将 SVG 拖拽指定区域进行转换

  1. 将转换好的 Android 格式的 vector 导入 AS

  1. 开始制作动画关联

//1.在 /res/aniamator 文件夹下 定义动画

<?xml version="1.0" encoding="utf-8"?>

<objectAnimator xmlns:android=“http://schemas.android.com/apk/res/android”
android:propertyName=“trimPathStart”
android:valueFrom=“1”
android:valueTo=“0”
android:duration=“2000”

//2. 在/res/drawable/ 定义 vector

<?xml version="1.0" encoding="utf-8"?>




//3. 在/res/drawable/ 关联动画和 vector

<?xml version="1.0" encoding="utf-8"?>



  1. 效果

警车灯闪烁

今日头条下拉刷新动画

来一个复杂组合动画,请看下面效果图:

  1. 准备 vector 数据




  1. 定义顺时针执行动画并做 pathData 变换

这里拿其中一个位置变化来举例说明:

<?xml version="1.0" encoding="utf-8"?>

<set xmlns:android=“http://schemas.android.com/apk/res/android”

android:ordering=“sequentially”>//按顺序执行

//依次执行 pathData 位置变换

<objectAnimator

android:duration=“600”
android:interpolator=“@android:interpolator/decelerate_cubic”
android:propertyName=“pathData”
android:valueFrom="
M100,30
L180,30
M180,30
L180,90
M180,90
L100,90
M100,90
L100,30"
android:valueTo="
M100,120
L180,120
M180,120
L180,180
M180,180
L100,180
M100,180
L100,120"
android:valueType=“pathType” />
<objectAnimator

android:duration=“600”
android:interpolator=“@android:interpolator/decelerate_cubic”
android:propertyName=“pathData”
android:valueFrom="
M100,120
L180,120
M180,120
L180,180
M180,180
L100,180
M100,180
L100,120"
android:valueTo="
M20,120
L100,120
M100,120
L100,180
M100,180
L20,180
M20,180
L20,120"
android:valueType=“pathType” />
<objectAnimator

android:duration=“600”
android:interpolator=“@android:interpolator/decelerate_cubic”
android:propertyName=“pathData”
android:valueFrom="
M20,120
L100,120
M100,120
L100,180
M100,180
L20,180
M20,180
L20,120"
android:valueTo="
M20,30
L100,30
M100,30
L100,90
M100,90
L20,90
M20,90
L20,30"
android:valueType=“pathType” />

如果对标签中的定义还不了解的先去看下文章中 path 标签 中的说明。如果不理解标签意思,根本就看不懂。

  1. 进行关联
<?xml version="1.0" encoding="utf-8"?>

<animated-vector xmlns:android=“http://schemas.android.com/apk/res/android”
xmlns:tools=“http://schemas.android.com/tools”
android:drawable=“@drawable/ic_toutiao”

tools:targetApi=“lollipop”>

  1. 代码控制重复执行

class SVGDemo1Activity : AppCompatActivity() {

var reStartTT = @SuppressLint(“HandlerLeak”)
object : Handler() {
override fun handleMessage(msg: Message) {
super.handleMessage(msg)
startAnimatabe(R.drawable.line_animated_toutiao, true)
}
}

@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_svg)

//水滴动画
startWaterDropAnimator.setOnClickListener {
startAnimatabe(R.drawable.line_animated_vector, false)
}
//搜索动画
startSearchAnimator.setOnClickListener {
startAnimatabe(R.drawable.line_animated_search, false)
}
//执行警车动画
startPoliceCarAnimator.setOnClickListener {
startAnimatabe(R.drawable.line_animated_car, false)
}
//执行头条动画
startTTAnimator.setOnClickListener {
startAnimatabe(R.drawable.line_animated_toutiao, true)
}
}

private fun startAnimatabe(lineAnimatedVector: Int, isRegister: Boolean): Animatable {
val animatedVectorDrawable = AnimatedVectorDrawableCompat.create(this, lineAnimatedVector)
iv.setImageDrawable(animatedVectorDrawable)
val animatable = iv.drawable as Animatable
animatable.start()
animatedVectorDrawable!!.registerAnimationCallback(object : Animatable2Compat.AnimationCallback() {
override fun onAnimationEnd(drawable: Drawable?) {
super.onAnimationEnd(drawable)
if (!isRegister) return
animatedVectorDrawable.unregisterAnimationCallback(this)
//重新开始在 xml 设置 restart 无效暂时用 Handler 实现了。
reStartTT.sendEmptyMessage(0)

}
})
return animatable

}
}

绘制中国地图

该篇之前实现 SVG pathData 都是利用 ImageView 来实现,并不是所有的场合都适合上面的方式,比如我想要实现 pathData 区域点击,那么上面所讲的方式应该是不能实现,下面我们以一个实例来看怎么自定义 View 实现 PathData 和 pathData 区域点击事件。

下面我们利用 path 来绘制一个中国地图,先来看一个最终效果图,如下:

看起来是不是很炫,还不错,嘿嘿,下面我们就来看一下如何实现。

  1. 准备地图 SVG
  • 首先去下载地图数据

  • 选择下载免费的地图数据

  • 找到对应的国家点击下载 svg 数据

  • 选择对应的地图数据,我这里下载的是高质量的 SVG

  1. SVG to Vector xml

将下载好的 china.svg 格式的文件转为 vector 节点的 xml 数据 或者用 AS 自带转也行,看个人爱好。

转好之后放入 AS 中,如下所示

现在有了这些数据,我们就可以解析 xml path 节点,拿到 pathData 数据我们不就可以绘制 path 了嘛。下面就开始解析 xml ,解析的方法很多种,我们这里用 dom 解析。

  1. 开始解析 xml

解析 xml 有很多种方式,这里就直接使用 DOM 解析,pathData2Path 我这里直接用 Android SDK 提供的 android.support.v4.graphics#PathParser 由于源码中它被标注了 hide 属性 ,我们需要直接将它 copy 到我们自己项目中, 具体转化请看如下代码:

/**

  • 开始解析 xml
    */
    public fun dom2xml(stream: InputStream?): MutableList {
    mapDataLists.clear()
    //dom
    val newInstance = DocumentBuilderFactory.newInstance()
    val newDocumentBuilder = newInstance.newDocumentBuilder()
    //拿到 Docment 对象
    val document = newDocumentBuilder.parse(stream)
    //获取 xml 中属于 path 节点的所有信息
    val elementsByTagName = document.getElementsByTagName(PATH_TAG)

//定义四个点,确定整个 map 的范围
var left = -1f
var right = -1f
var top = -1f
var bottom = -1f
//开始遍历标签,拿到 path 数据组
for (pathData in 0 until elementsByTagName.length) {
val item = elementsByTagName.item(pathData) as Element
val name = item.getAttribute(“android:name”)
val fillColor = item.getAttribute(“android:fillColor”)
val strokeColor = item.getAttribute(“android:strokeColor”)
val strokeWidth = item.getAttribute(“android:strokeWidth”)
val pathData = item.getAttribute(“android:pathData”)
val path = PathParser.createPathFromPathData(pathData)
mapDataLists.add(MapData(name, fillColor, strokeColor, strokeWidth, path))
//获取控件的宽高
val rect = RectF()
//获取到每个省份的边界
path.computeBounds(rect, true)
//遍历取出每个path中的left取所有的最小值
left = if (left == -1f) rect.left else Math.min(left, rect.left)
//遍历取出每个path中的right取所有的最大值
right = if (right == -1f) rect.right else Math.max(right, rect.right)
//遍历取出每个path中的top取所有的最小值
top = if (top == -1f) rect.top else Math.min(top, rect.top)
//遍历取出每个path中的bottom取所有的最大值
bottom = if (bottom == -1f) rect.bottom else Math.max(bottom, rect.bottom)
}
//MAP 的矩形区域
MAP_RECTF = RectF(left, top, right, bottom)
return mapDataLists;
}
复制代码

  1. 进行控件测量适配横竖屏切换和宽高定义 wrap_content 模式

/**

  • 开始测量
    */
    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec)
    //测量模式
    var widthMode = MeasureSpec.getMode(widthMeasureSpec)
    var heightMode = MeasureSpec.getMode(heightMeasureSpec)
    //测量大小
    widthSize = MeasureSpec.getSize(widthMeasureSpec)
    heightSize = MeasureSpec.getSize(heightMeasureSpec)

if (!MAP_RECTF.isEmpty && mMapRectHeight != 0f && mMapRectWidth != 0f) {
//显示比例
scaleHeightValues = heightSize / mMapRectHeight
scaleWidthValues = widthSize / mMapRectWidth
}

//xml 文件中宽高 wrap_content
if (widthMode == MeasureSpec.AT_MOST || heightMode == MeasureSpec.AT_MOST) {
//如果是横屏宽保留最大,高需要适配
if (widthSize < heightSize && mMapRectHeight != 0f) {
setMeasuredDimension(widthSize, (mMapRectHeight * scaleWidthValues).toInt())
} else {
setMeasuredDimension(widthSize, heightSize)
}
} else {
setMeasuredDimension(widthSize, heightSize)
}
}

  1. 开始绘制 path

/**

  • 绘制 Map 数据
    */
    @SuppressLint(“Range”)
    private fun drawMap(canvas: Canvas) {
    canvas.save()
    if (widthSize > heightSize) {
    canvas.scale(scaleWidthValues, scaleHeightValues)
    } else {
    canvas.scale(scaleWidthValues, scaleWidthValues)
    }

mapDataList.forEach { data ->
run {
if (data.isSelect) {
drawPath(data, canvas, Color.RED)
} else {
drawPath(data, canvas, Color.parseColor(data.fillColor))
}
}
}
canvas.restore()
canvas.drawText(“中国🇨🇳地图”, widthSize / 2 - mPaintTextTitle.measureText(“中国🇨🇳地图”) / 2f, 100f, mPaintTextTitle)
}

/**

  • 开始绘制 Path
    */
    private fun drawPath(
    data: MapData,
    canvas: Canvas,
    magenta: Int
    ) {
    mPaintPath.setColor(magenta)
    mPaintPath.setStyle(Paint.Style.FILL)
    mPaintPath.setTextSize(30f)
    mPaintPath.setStrokeWidth(data.strokeWidth.toFloat())
    canvas.drawPath(data.pathData, mPaintPath)
    val rectF = RectF()
    data.pathData.computeBounds(rectF, true)
    canvas.drawText(
    if (data.name.isEmpty()) “” else data.name,
    rectF.centerX() - mPaintText.measureText(data.name) / 2,
    rectF.centerY(), mPaintText
    )
    }
  1. 给地图添加各自的点击事件

override fun onTouchEvent(event: MotionEvent): Boolean {
when (event.action) {
MotionEvent.ACTION_DOWN -> return true
MotionEvent.ACTION_UP -> {
handlerTouch(event.getX(), event.getY())
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

img

img

img

img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

尾声

如果你想成为一个优秀的 Android 开发人员,请集中精力,对基础和重要的事情做深度研究。

对于很多初中级Android工程师而言,想要提升技能,往往是自己摸索成长,不成体系的学习效果低效漫长且无助。 整理的这些架构技术希望对Android开发的朋友们有所参考以及少走弯路,本文的重点是你有没有收获与成长,其余的都不重要,希望读者们能谨记这一点。

这里,笔者分享一份从架构哲学的层面来剖析的视频及资料给大家梳理了多年的架构经验,筹备近6个月最新录制的,相信这份视频能给你带来不一样的启发、收获。

Android进阶学习资料库

一共十个专题,包括了Android进阶所有学习资料,Android进阶视频,Flutter,java基础,kotlin,NDK模块,计算机网络,数据结构与算法,微信小程序,面试题解析,framework源码!

大厂面试真题

PS:之前因为秋招收集的二十套一二线互联网公司Android面试真题 (含BAT、小米、华为、美团、滴滴)和我自己整理Android复习笔记(包含Android基础知识点、Android扩展知识点、Android源码解析、设计模式汇总、Gradle知识点、常见算法题汇总。)

《2019-2021字节跳动Android面试历年真题解析》

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

kr-1712696910300)]

Android进阶学习资料库

一共十个专题,包括了Android进阶所有学习资料,Android进阶视频,Flutter,java基础,kotlin,NDK模块,计算机网络,数据结构与算法,微信小程序,面试题解析,framework源码!

[外链图片转存中…(img-FZlpZhIX-1712696910300)]

大厂面试真题

PS:之前因为秋招收集的二十套一二线互联网公司Android面试真题 (含BAT、小米、华为、美团、滴滴)和我自己整理Android复习笔记(包含Android基础知识点、Android扩展知识点、Android源码解析、设计模式汇总、Gradle知识点、常见算法题汇总。)

[外链图片转存中…(img-VypigeWH-1712696910300)]

《2019-2021字节跳动Android面试历年真题解析》

[外链图片转存中…(img-f9JnWCcQ-1712696910301)]

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

  • 13
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值