
Github链接
画2D图形有两种方法:
Canvas实际上是封装了各种draw方法的类,调用draw方法把图形绘制到底层的Surface上,即绘制在Window上。
这个例子中构造了两个Canvas和一个Bitmap,分别调用其draw方法,先是mCanvas往Bitmap里绘制一个方块,再在onDraw方法内调用canvasdrawBitmap绘制这个方块。
思考一个问题,为什么mCanvas需要设置Bitmap?
很简单,因为它没有持有一块内存地址,自然没法绘制。来看一下draw的起点ViewRootImpl(软件绘制,不开启硬件加速下)。
这个通过mSurfacelockCanvas返回的Canvas是Viewdraw的canvas变量,所以当1,2情况时,Canvas都持有一个Bitmap,指向共享内存里的某一小块,当调用Canvasdraw方法时就能绘制出东西。但对于自定义Canvas来说并不是,即使设置一个Bitmap和绘制了Bitmap,但不往共享内存上写,屏幕上是不会显示的,SurfaceView同理,通过SurfacelockCanvas获取持有共享内存的Canvas,绘制完毕后调用SurfaceunlockCanvasAndPost把绘制内容显示到surface上并release掉Canvas。
顺带一提Canvassave和Canvasrestore方法,如下Demo
效果图如
画的是三个颜色和旋转角度都不同的小方形。
步骤1把默认坐标系旋转20°,画出第一个蓝色的方形,步骤2保存当前的matrix(旋转了20°),继续旋转20°,此时坐标系已经旋转了40°,画出第二个**的方块,步骤3,恢复上一步保存的matrix(旋转了20°),此时坐标系还是旋转了20°,步骤4,再旋转40°,此时坐标系旋转了60°,画出第三个黑色方块。
Canvassave用于保存当前matrix和clip,Canvasrestore用于恢复上次保存的matrix和clip。
Drawable是一个能画出来的物体的抽象,使用前需要调用setBounds确定位置和大小,通过getIntrinsicHeight和getIntrinsicWidth取到实际大小。Drawable可以有几种形式存在:Bitmap、Nine Patch、Vector、Shape、Layers等。
从ResourcegetDrawable会判断是否xml结尾,不是的话走6,7步,如果从xml中读取,需要getResourcegetDrawable -> ResourceImplloadDrawableForCookie -> drawablecreateFromXml -> DrawableInflaterinflateFromXmlForDensity -> drawableinflateFromTag
看一下Shape实现类GradientDrawable的inflate实现,读取各项属性并赋值,到draw方法。
调用canvasdrawRect把mRect画出来,而mRect的赋值在ensureValidRect。[上传失败(image-a25af0-1515826613001)]
bounds在哪里设置的?答案是ImageViewupdateDrawable内,会调用DrawablegetIntrinsicHeight赋值(从xml中size属性读取),再调用configureBounds -> setBounds,如果使用的不是ImageView,一定要在draw之前 调用setBounds ,否则size就会出错。
回到loadDrawableForCookie,再看一下6,7步加载的过程,通过AssetManager读取流数据,通过DrawablecreateFromResourceStream这个我们经常使用的方法获取到Drawable。
取到屏幕密度之后调用BitmapFactorydecodeResourcesStream,计算密度后调用native创建Bitmap,感兴趣的同学可以看下更具体的分析文章(如 理解Bitmap )。
本文探究了两点
Android 711 源码
Android 官方文档, Canvas and Drawable , Drawable 等
做安卓开发的话,不会自定义view是不行的,自定定义各种控件以满足开发需求,在开发中是很重要的,自定义view通过继承view,通过重写ondraw方法实现重绘自己所需要的控件样式。
在ondraw方法中,通过canvas来绘制想要的样式,首先需要定义好画笔,以及画笔的各种属性,比如需要的时候要
抗锯齿
等等。都准备好了就可以用canvas来实现绘图了,当然api提供的api肯定是不够用的,需要多姿多彩的样式很多时候需要借助准备好的一些,通过canvas绘制bitmap来实现把准备好的绘制上去。绘制好了当然还是不够的,控件都是需要和用户交互的,所以很多时候样式是会发生改变的,所以要在其中定义相关方法暴露出来,方法中处理用户 *** 作或其他的结果改变样式的重绘,绘制好了调用更新(
invalidate
())方法,实现样式的改变。做好一个控件还需要优化性能等等,都需要一步一慢慢实现。
找到两个前端就能解决的方法,最后因为各种原因采用了方法二。
方法一:
找到地图上的全部点,然后在canvas上面重绘一次。
html2canvas(this$refstarget, {
useCORS: true, // 如果截图的内容里有,可能会有跨域的情况,加上这个参数,解决文件跨域问题
})then((canvas) => {
let cans = canvasgetContext("2d");
//批量地图重新打点 加载
documentquerySelectorAll("#mapView_layers image")forEach((item) => {
var obj = item;
var x = itemgetAttribute("x");
var y = itemgetAttribute("y");
var itemWidth = itemgetAttribute("width");
var itemHeight = itemgetAttribute("height");
consolelog("item", item, x, y);
if (width == 8) {
cansdrawImage(obj, x, y, itemWidth, itemHeight);
} else {
cansdrawImage(
obj,
x ,
y - 1 - itemHeight / 2 ,
itemWidth,
itemHeight
);
}
});
//下面是截图代码
})
登录后复制
因为本身目标dom的position定位问题,最后打的点可能会出现偏移。
所以还要给html2canvas加几个属性: x , y , scrollX , scrollY。保险起见,再加上两个参数 width 和 height 。
本人是后面chrome测着没问题,但是给小伙伴测试的时候,他用的360浏览器还有个xx浏览器有点问题。干脆参数全加上。
screenShot() {
let canvasBox = this$refstarget;
//获取目标div位置;
var tPosition = canvasBoxgetBoundingClientRect();
consolelog("size", tPosition);
// 获取父级的宽高
const width = parseInt(windowgetComputedStyle(canvasBox)width);
const height = parseInt(windowgetComputedStyle(canvasBox)height);
html2canvas(this$refstarget, {
width: width,
height: height,
x: 0,
y: 0,
scrollY: -tPositiony,
scrollX: -tPositionx,
useCORS: true, // 如果截图的内容里有,可能会有跨域的情况,加上这个参数,解决文件跨域问题
})then((canvas) => {
})
}
登录后复制
要是项目的地图是不可移动的,基本到这里就可以了。
但是地图只要一挪动。。一个新的bug出现了。。。。。整个地图画线打点层的偏移量和截图之前不一样。。。。 截图后,画线层偏的比原地图还要远,打点却还在原位没动过。。
这个问题需要修正svg的偏移,然后这个标注点绘制的时候也要加上一个偏移量。
地图偏移的bug后面再讲。
方法二:(最后采用)
把svg中所有的<image>的href路径转换为base64编码格式。简单方便,不用考虑位置什么的问题,就是有些浏览器里面加载慢。。。setTimeout有时候要设置大一点。。
screenShot() {
let canvasBox = this$refstarget;
//获取目标div位置;
var tPosition = canvasBoxgetBoundingClientRect();
consolelog("size", tPosition);
// 获取父级的宽高
const width = parseInt(windowgetComputedStyle(canvasBox)width);
const height = parseInt(windowgetComputedStyle(canvasBox)height);
//---------------------
//解决svg 内部image加载不了的问题,把image改为base64,配合setTimeout html2canvas使用
documentquerySelectorAll("#mapView_layers image")forEach((item) => {
consolelog("item", item);
var img = itemgetAttribute("xlink:href");
consolelog("href", img);
var image = new Image();
imagecrossOrigin = "";
imagesrc = img;
imageonload = () => {
var base64 = getBase64Image(image);
itemsetAttribute("xlink:href", base64); //更改href属性
};
});
//地址转为base64编码
function getBase64Image(img) {
var canvas = documentcreateElement("canvas");
canvaswidth = imgwidth;
canvasheight = imgheight;
var ctx = canvasgetContext("2d");
ctxdrawImage(img, 0, 0, imgwidth, imgheight);
var ext = imgsrcsubstring(imgsrclastIndexOf("") + 1)toLowerCase();
var dataURL = canvastoDataURL("image/" + ext);
return dataURL;
}
setTimeout(() => {
html2canvas(this$refstarget, {
width: width,
height: height,
x: 0,
y: 0,
scrollY: -tPositiony,
scrollX: -tPositionx,
useCORS: true, // 如果截图的内容里有,可能会有跨域的情况,加上这个参数,解决文件跨域问题
})then((canvas) => {
})
}, 200);
}
登录后复制
Canvas在我的理解中就好像在一张画布上绘制图像,它只能看到却“摸”不到,那要如何进行 *** 作呢。我不知道网上是怎么做的,这里用自己的想法做了个DEMO分享给大家。 思路: 虽然Canvas不能拖拽,但div可以拖拽,那怎么把二者结合起来呢。初步想法是
Canvas 组件提供两种方法让你指定或者获取画布对象: Item handles 、 tags 。
调用: Canvas(master=None, cnf={}, kw)
为 Canvas 组件中所有的画布对象添加 tag,该方法相当于 addtag(tag, "all") 。
为显示列表中 item 下方的画布对象添加 Tag,该方法相当于 addtag(tag, "below", item) ; item 可以是单个画布对象的 ID,也可以是某个 tag。
将 tag 添加到与给定(画布坐标系的)坐标 相临近的画布对象(该方法相当于 addtag(tag, "closet", x, y,halo=None, start=None) )。可选参数 halo 指定一个距离,表示以 为中心,该距离内的所有画布对象均添加 tag;可选参数 start 指定一个画布对象,该方法将为低于但最接近该对象的画布对象添加 tag。 [2]
为所有坐标在矩形 中的画布对象添加 tag,该方法相当于 addtag(tag, "enclosed", x0, y0, x1, y1) 。
跟 addtag_enclosed() 方法相似,不过该方法范围更广(即使画布对象只有一部分在矩形中也算),该方法相当于 addtag(tag, "overlapping", x0, y0, x1, y1) 。
为 item 参数指定的画布对象添加 tag ,该方法相当于 addtag(tag, "withtag", item) 。 item 参数如果指定一个 tag ,则为所有拥有此 tag 的画布对象添加新的 tag;item 参数如果指定一个画布对象,那么只为其添加 tag。
返回一个四元组 用于描述 args 指定的画布对象所在的矩形范围,如果 args 参数省略,则返回所有的画布对象所在的矩形范围。
将窗口坐标系的 X 坐标(screenx)转化为画布坐标系。如果提供 gridspacing 参数,则转换结果将为该参数的整数倍。
将窗口坐标系的 Y 坐标(screeny)转化为画布坐标系。如果提供 gridspacing 参数,则转换结果将为该参数的整数倍。
如果仅提供一个参数(画布对象),返回该画布对象的坐标 。可以通过 coords(item, x1, y1, x2, y2) 来移动画布对象。
删除 item 中从 from 到 to (包含)参数中的字符串。 item 可以是单个画布对象的 ID,也可以是某个 tag。
删除 item 参数指定的画布对象。如果不存在 item 指定的画布对象,并不会产生错误; item 可以是单个画布对象的 ID,也可以是某个 tag。
在 item 参数指定的画布对象中删除指定的 tag。如果 tag 参数被忽略,则删除指定画布对象所有的tags;如果不存在 item 指定的画布对象,并不会产生错误。item 可以是单个画布对象的 ID,也可以是某个 tag。
返回在 item 参数指定的画布对象之上的 ID。如果有多个画布对象符合要求,那么返回最顶端的那个;如果 item 参数指定的是最顶层的画布对象,那么返回一个空元组。item 可以是单个画布对象的 ID,也可以是某个tag。
返回 Canvas 组件上所有的画布对象。返回格式是一个元组,包含所有画布对象的 ID。按照显示列表的顺序返回。该方法相当于 find_withtag('all') 。
返回在 item 参数指定的画布对象之下的 ID。如果有多个画布对象符合要求,那么返回最底端的那个。如果 item 参数指定的是最底层的画布对象,那么返回一个空元组。item 可以是单个画布对象的 ID,也可以是某个 tag。
返回一个元组,包含所有靠近点(x, y)的画布对象的ID。如果没有符合的画布对象,则返回一个空元组。可选参数 halo 用于增加点(x, y)的辐射范围。可选参数 start 指定一个画布对象,该方法仅返回在显示列表中低于但最接近的一个 ID。注意,点(x, y)的坐标是采用画布坐标系来表示。
返回完全包含在限定矩形内所有画布对象的 ID。
返回所有与限定矩形有重叠的画布对象的 ID(也包含在限定矩形内的画布对象)
返回 item 指定的所有画布对象的 ID。item 可以是单个画布对象的 ID,也可以是某个tag
将焦点移动到指定的 item。如果有多个画布对象匹配,则将焦点移动到显示列表中第一个可以接受光标输入的画布对象。item 可以是单个画布对象的 ID,也可以是某个tag
返回与 item 相关联的所有 tags。item 可以是单个画布对象的 ID,也可以是某个 tag
将光标移动到 item 指定的画布对象。这里要求 item 指定的画布对象支持文本输入和转移焦点。 item 可以是单个画布对象的 ID,也可以是某个tag
返回 index 在指定 item 中的位置(沿用 Python 的惯例:0 表示第一)。index 参数可以是:INSERT(当前光标的位置),END(最后一个字符的位置),SEL_FIRST(当前选中文本的起始位置),SEL_LAST(当前选中文本的结束位置),还可以使用格式为 "@x, y" (x 和 y 是画布坐标系)来获得与此坐标最接近的位置。item 可以是单个画布对象的 ID,也可以是某个 tag
在允许进行文本编辑的画布对象的指定位置插入文本。index 参数可以是:INSERT(当前光标的位置),END(最后一个字符的位置),SEL_FIRST(当前选中文本的起始位置),SEL_LAST(当前选中文本的结束位置),还可以使用格式为 "@x, y"(x 和 y 是画布坐标系)来获得与此坐标最接近的位置-- item 可以是单个画布对象的 ID,也可以是某个 tag
获得指定 item 的选项的当前值。item 可以是单个画布对象的 ID,也可以是某个 tag
修改指定 item 的选项的当前值-- item 可以是单个画布对象的 ID,也可以是某个 tag
跟 itemconfig() 一样
将指定画布对象移动到显示列表的顶部。item 可以是单个画布对象的 ID,也可以是某个tag。跟 tag_raise 一样
将指定画布对象移动到显示列表的底部。item 可以是单个画布对象的 ID,也可以是某个 tag。跟 tag_lower 一样
将 item 移动到新位置(x, y)。item 可以是单个画布对象的 ID,也可以是某个 tag。
将 Canvas 的当前内容封装成 PostScript格式( 什么是 PostScript )表示
下方表格列举了各个 options 选项的具体含义:
缩放 item 指定的画布对象。xOrigin 和 yOrigin 决定要缩放的位置;xScale 和 yScale 决定缩放的比例;item 可以是单个画布对象的 ID,也可以是某个 tag。
注意:该方法无法缩放 Text 画布对象
可以看一个例子:
调整选中范围,使得给定的 index 参数指定的位置在范围内。item 可以是单个画布对象的 ID,也可以是某个 tag
取消 Canvas 组件中所有选中的范围
调整选中范围的起始位置为 index 参数指定的位置。item 可以是单个画布对象的 ID,也可以是某个 tag
范围在 Canvas 组件中当前文本的选中范围。如果没有则返回 None
调整选中范围的结束位置为 index 参数指定的位置
为 Canvas 组件上的画布对象绑定方法。event 参数是绑定的事件名称,callback 是与之关联的方法。item 可以是单个画布对象的 ID,也可以是某个Tag
注意:与绑定事件关联的是画布对象,而不是 Tag
将一个或多个画布对象移至底部。如果是多个画布对象,将它们都移至底部并保留原有顺序。item 可以是单个画布对象的 ID,也可以是某个Tag
注意:该方法对窗口组件无效,请使用 lower 代替
将一个或多个画布对象移至顶部-- 如果是多个画布对象,将它们都移至顶部并保留原有顺序。item 可以是单个画布对象的 ID,也可以是某个Tag
注意:该方法对窗口组件无效,请使用 lift 代替
解除与 item 绑定的事件。item 可以是单个画布对象的 ID,也可以是某个Tag
将指定画布对象移动到显示列表的顶部。item 可以是单个画布对象的 ID,也可以是某个Tag。跟 tag_raise 一样
返回指定画布对象的类型。返回值可以是:"arc", "bitmap","image", "line", "oval", "polygon","rectangle", "text", 或"window"
该方法用于在水平方向上滚动 Canvas 组件的内容,一般通过绑定 Scollbar 组件的 command 选项来实现(具体 *** 作参考:Scrollbar)
-- 如果第一个参数是 MOVETO,则第二个参数表示滚动到指定的位置:00 表示最左端,10 表示最右端
-- 如果第一个参数是 SCROLL,则第二个参数表示滚动的数量,第三个参数表示滚动的单位(可以是 UNITS 或 PAGES),例如:xview(SCROLL,3, UNITS) 表示向右滚动三行
-- 跟 xview(MOVETO, fraction) 一样
跟 xview(SCROLL, number, what) 一样
该方法用于在垂直方向上滚动 Canvas 组件的内容,一般通过绑定 Scollbar 组件的 command 选项来实现(具体 *** 作参考:Scrollbar)-- 如果第一个参数是 MOVETO,则第二个参数表示滚动到指定的位置:00 表示最顶端,10 表示最底端-- 如果第一个参数是 SCROLL,则第二个参数表示滚动的数量,第三个参数表示滚动的单位(可以是 UNITS 或 PAGES),例如:yview(SCROLL,3, PAGES) 表示向下滚动三页
跟 yview(MOVETO, fraction) 一样
跟 yview(SCROLL, number, what) 一样
使用tkinter中创建canvas时,会设置canvas的宽高。一般我们认为canvas中画图区域就是设置的宽高。其实这不太正确,canvas还有一个边框,如果不另外设置,真正的画图区域要减去边框。
比如我们容器的尺寸为width, height。然后在这个容器中添加一个唯一的canvas,canvas的尺寸设置为width, height,并且pack(expand=1, fill=both)。如果给这个canvas设置一个背景色,容器外部设置另外一个背景色,就可以看得到canvas四周有一条白色的边框。这在我们做窗口布置时,显得不太完美。改进方法其实很简单:
见下方 scan_mark(x, y)
使用这种方式来实现 Canvas 内容的滚动。需要将鼠标按钮事件及当前鼠标位置绑定到 scan_mark(x, y) 方法,然后再将 <motion> 事件及当前鼠标位置绑定到 scan_dragto(x,y,gain=10) 方法,就可以实现 Canvas 在当前位置和 sacn_mack(x, y) 指定的位置 (x, y) 之间滚动
我们知道,Activity 是在 ActivityThread 的 performLaunchActivity 中进行创建的,在创建完成之后就会调用其 attach 方法,它是先于 onCreate、onStart、onResume 等生命周期函数的,因此将 attach 方法作为这篇文章主线的开头:
attach() 方法就是 new 一个 PhoneWindow 并且关联 WindowManager。
接下来就到了 onCreate 方法:
这一步就是把我们的布局文件解析成 View 塞到 DecorView 的一个 id 为 Ridcontent 的 ContentView 中,DecorView 本身是一个 FrameLayout,它还承载了 StatusBar、NavigationBar 。
然后在 handleResumeActivity 中,通过 WindowManager 的 addView 方法把 DecorView 添加进去,实际实现是 WindowManagerImpl 的 addView 方法,它里面再通过 WindowManagerGlobal 的实例去 addView 的,在它里面就会 new 一个 ViewRootImpl,也就是说最后是把 DecorView 传给了 ViewRootImpl 的 setView 方法。ViewRootImpl 是 DecorView 的管理者,它负责 View 树的测量、布局、绘制,以及通过 Choreographer 来控制 View 的刷新。
WMS 是所有 Window 窗口的管理员,负责 Window 的添加和删除、Surface 的管理和事件派发等等,因此每一个 Activity 中的 PhoneWindow 对象如果需要显示等 *** 作,就必须要与 WMS 交互才能进行。
在 ViewRootImpl 的 setView 方法中,会调用 requestLayout,并且通过 WindowSession 的 addToDisplay 与 WMS 进行交互。WMS 会为每一个 Window 关联一个 WindowStatus。
SurfaceFlinger 主要是进行 Layer 的合成和渲染。
在 WindowStatus 中,会创建 SurfaceSession,SurfaceSession 会在 Native 层构造一个 SurfaceComposerClient 对象,它是应用程序与 SurfaceFlinger 沟通的桥梁。
经过步骤四和步骤五之后,ViewRootImpl 与 WMS、SurfaceFlinger 都已经建立起连接,但此时 View 还没显示出来,我们知道,所有的 UI 最终都要通过 Surface 来显示,那么 Surface 是什么时候创建的呢?
这就要回到前面所说的 ViewRootImpl 的 requestLayout 方法了,首先会 checkThread 检查是否是主线程,然后调用 scheduleTraversals 方法,scheduleTraversals 方法会先设置同步屏障,然后通过 Choreographer 类在下一帧到来时去执行 doTraversal 方法。简单来说,Choreographer 内部会接受来自 SurfaceFlinger 发出的 Vsync 垂直同步信号,这个信号周期一般是 16ms 左右。doTraversal 方法首先会先移除同步屏障,然后 performTraversals 真正进行 View 的绘制流程,即调用 performMeasure、performLayout、performDraw。不过在它们之前,会先调用 relayoutWindow 通过 WindowSession 与 WMS 进行交互,即把 Java 层创建的 Surface 与 Native 层的 Surface 关联起来。
接下来就是正式绘制 View 了,从 performTraversals 开始,Measure、Layout、Draw 三步走。
第一步是获取 DecorView 的宽高的 MeasureSpec 然后执行 performMeasure 流程。MeasureSpec 简单来说就是一个 int 值,高 2 位表示测量模式,低 30 位用来表示大小。策略模式有三种,EXACTLY、AT_MOST、UNSPECIFIED。EXACTLY 对应为 match_parent 和具体数值的情况,表示父容器已经确定 View 的大小;AT_MOST 对应 wrap_content,表示父容器规定 View 最大只能是 SpecSize;UNSPECIFIED 表示不限定测量模式,父容器不对 View 做任何限制,这种适用于系统内部。接着说,performMeasure 中会去调用 DecorView 的 measure 方法,这个是 View 里面的方法并且是 final 的,它里面会把参数透传给 onMeasure 方法,这个方法是可以重写的,也就是我们可以干预 View 的测量过程。在 onMeasure 中,会通过 getDefaultSize 获取到宽高的默认值,然后调用 setMeasureDimension 将获取的值进行设置。在 getDefaultSize 中,无论是 EXACTLY 还是 AT_MOST,都会返回 MeasureSpec 中的大小,这个 SpecSize 就是测量后的最终结果。至于 UNSPECIFIED 的情况,则会返回一个建议的最小值,这个值和子元素设置的最小值以及它的背景大小有关。从这个默认实现来看,如果我们自定义一个 View 不重写它的 onMeasure 方法,那么 warp_content 和 match_parent 一样。所以 DecorView 重写了 onMeasure 函数,它本身是一个 FrameLayout,所以最后也会调用到 FrameLayout 的 onMeasure 函数,作为一个 ViewGroup,都会遍历子 View 并调用子 View 的 measure 方法。这样便实现了层层递归调用到了每个子 View 的 onMeasure 方法进行测量。
第二步是执行 performLayout 的流程,也就是调用到 DecorView 的 layout 方法,也就是 View 里面的方法,如果 View 大小发生变化,则会回调 onSizeChanged 方法,如果 View 状态发生变化,则会回调 onLayout 方法,这个方法在 View 中是空实现,因此需要看 DecorView 的父容器 FrameLayout 的 onLayout 方法,这个方法就是遍历子 View 调用其 layout 方法进行布局,子 View 的 layout 方法被调用的时候,它的 onLayout 方法又会被调用,这样就布局完了所有的 View。
第三步就是 performDraw 方法了,里面会调用 drawSoftware 方法,这个方法需要先通过 mSurface lockCanvas 获取一个 Canvas 对象,作为参数传给 DecorView 的 draw 方法。这个方法调用的是 View 的 draw 方法,先绘制 View 背景,然后绘制 View 的内容,如果有子 View 则会调用子 View 的 draw 方法,层层递归调用,最终完成绘制。
完成这三步之后,会在 ActivityThread 的 handleResumeActivity 最后调用 Activity 的 makeVisible,这个方法就是将 DecorView 设置为可见状态。
>
以上就是关于Android绘图基础--Canvas和Drawable全部的内容,包括:Android绘图基础--Canvas和Drawable、android:如何用canvas在自定义view里画图、gis多个图层地图用htmlcanvas截图获取不到等相关内容解答,如果想了解更多相关内容,可以关注我们,你们的支持是我们更新的动力!
欢迎分享,转载请注明来源:内存溢出
微信扫一扫
支付宝扫一扫
评论列表(0条)