zoukankan      html  css  js  c++  java
  • 安卓自己定义View进阶-Path基本操作

    版权声明:本人全部文章均採用 [知识共享 署名-非商业性使用-禁止演绎 4.0 国际 许可协议] 转载前请保证理解此协议,原文出处 :http://www.gcssloop.com/#blog https://blog.csdn.net/u013831257/article/details/50784565

    Path之基本操作

    作者微博: @GcsSloop

    【本系列相关文章】

    在上一篇Canvas之图片文字中我们了解了怎样使用Canvas中绘制图片文字。结合前几篇文章,Canvas的基本操作已经几乎相同完结了。然而Canvas不仅仅具有这些主要的操作,还能够更加炫酷。本次会了解到path(路径)这个Canvas中的神器,有了这个神器,就能创造出很多其它炫(zhuang)酷(B)的东东了。


    一.Path经常用法表

    为了兼容性(偷懒) 本表格中去除了部分API21(即安卓版本号5.0)以上才加入的方法。

    作用 相关方法 备注
    移动起点 moveTo 移动下一次操作的起点位置
    设置终点 setLastPoint 重置当前path中最后一个点位置,假设在绘制之前调用,效果和moveTo同样
    连接直线 lineTo 加入上一个点到当前点之间的直线到Path
    闭合路径 close 连接第一个点连接到最后一个点,形成一个闭合区域
    加入内容 addRect, addRoundRect, addOval, addCircle, addPath, addArc, arcTo 加入(矩形, 圆角矩形, 椭圆, 圆, 路径。 圆弧) 到当前Path (注意addArc和arcTo的差别)
    是否为空 isEmpty 推断Path是否为空
    是否为矩形 isRect 推断path是否是一个矩形
    替换路径 set 用新的路径替换到当前路径全部内容
    偏移路径 offset 对当前路径之前的操作进行偏移(不会影响之后的操作)
    贝塞尔曲线 quadTo, cubicTo 分别为二次和三次贝塞尔曲线的方法
    rXxx方法 rMoveTo, rLineTo, rQuadTo, rCubicTo 不带r的方法是基于原点的坐标系(偏移量), rXxx方法是基于当前点坐标系(偏移量)
    填充模式 setFillType, getFillType, isInverseFillType, toggleInverseFillType 设置,获取,推断和切换填充模式
    提示方法 incReserve 提示Path还有多少个点等待加入(这种方法貌似会让Path优化存储结构)
    布尔操作(API19) op 对两个Path进行布尔运算(即取交集、并集等操作)
    计算边界 computeBounds 计算Path的边界
    重置路径 reset, rewind 清除Path中的内容
    reset不保留内部数据结构,但会保留FillType.
    rewind会保留内部的数据结构,但不保留FillType
    矩阵操作 transform 矩阵变换

    二.Path具体解释

    请关闭硬件加速,以免引起不必要的问题。
    请关闭硬件加速,以免引起不必要的问题。
    请关闭硬件加速,以免引起不必要的问题!

    在AndroidMenifest文件里application节点下添上 android:hardwareAccelerated=”false”以关闭整个应用的硬件加速。
    很多其它请參考这里:Android的硬件加速及可能导致的问题

    Path作用

    本次特地开了一篇具体解说Path,为什么要单独摘出来呢,这是因为Path在2D画图中是一个非常重要的东西。

    在前面我们解说的全部绘制都是简单图形(如 矩形 圆 圆弧等),而对于那些复杂一点的图形则没法去绘制(如绘制一个心形 正多边形 五角星等)。而使用Path不仅能够绘制简单图形。也能够绘制这些比較复杂的图形。另外,依据路径绘制文本和剪裁画布都会用到Path。

    关于Path的作用先简单地说这么多,具体的我们接下来慢慢研究。

    Path含义

    官方介绍:

    The Path class encapsulates compound (multiple contour) geometric paths consisting of straight line segments, quadratic curves, and cubic curves. It can be drawn with canvas.drawPath(path, paint), either filled or stroked (based on the paint’s Style), or it can be used for clipping or to draw text on a path.

    嗯,没错依然是拿来装逼的,假设你看不懂的话,不用操心。事实上并没有什么卵用。

    通俗解释(sloop个人版):

    Path是封装了由直线和曲线(二次,三次贝塞尔曲线)构成的几何路径。

    你能用Canvas中的drawPath来把这条路径画出来(同样支持Paint的不同绘制模式),也能够用于剪裁画布和依据路径绘制文字。我们有时会用Path来描写叙述一个图像的轮廓。所以也会称为轮廓线(轮廓线仅是Path的一种用法。两者并不等价)

    另外路径有开放和封闭的差别。

    图像 名称 备注
    封闭路径 首尾相接形成了一个封闭区域
    开放路径 没有首位相接形成封闭区域

    这个是我随便画的。仅为展示一下差别,请无视我灵魂画师一般的画图水准。

    与Path相关的另一些比較奇妙的概念,只是暂且不说,等接下来须要用到的时候再具体说明。

    Path用法具体解释

    前面扯了一大堆概念性的东西。

    接下来就開始实战了。请诸位看官准备好瓜子、花生、爆米花。坐下来慢慢观看。

    第1组: moveTo、 setLastPoint、 lineTo 和 close

    因为Path的有些知识点无法单独来讲,所以本次採取了一次讲一组方法。

    依照惯例。先创建画笔:

            Paint mPaint = new Paint();             // 创建画笔
            mPaint.setColor(Color.BLACK);           // 画笔颜色 - 黑色
            mPaint.setStyle(Paint.Style.STROKE);    // 填充模式 - 描边
            mPaint.setStrokeWidth(10);              // 边框宽度 - 10

    lineTo:

    方法预览:

    public void lineTo (float x, float y)

    首先解说的的LineTo。为啥先解说这个呢?

    是因为moveTo、 setLastPoint、 close都无法直接看到效果。借助有具现化效果的lineTo才干让这些方法现出原形。

    lineTo非常简单,仅仅有一个方法,作用也非常easy理解,line嘛。顾名思义就是一条线。

    俗话(数学书上)说。两点确定一条直线,可是看參数明显仅仅给了一个点的坐标吧(这不按常理出牌啊)。

    再细致一看。这个lineTo除了line外另一个to呢,to翻译过来就是“至”,到某个地方的意思。lineTo难道是指从某个点到參数坐标点之间连一条线?

    没错。你猜对了。可是这某个点又是哪里呢?

    前面我们提到过Path能够用来描写叙述一个图像的轮廓。图像的轮廓通常都是用一条线构成的。所以这里的某个点就是上次操作结束的点,假设没有进行过操作则默认点为坐标原点。

    那么我们就来试一下:

            canvas.translate(mWidth / 2, mHeight / 2);  // 移动坐标系到屏幕中心(宽高数据在onSizeChanged中获取)
    
            Path path = new Path();                     // 创建Path
    
            path.lineTo(200, 200);                      // lineTo
            path.lineTo(200,0);
    
            canvas.drawPath(path, mPaint);              // 绘制Path

    在演示样例中我们调用了两次lineTo,第一次因为之前没有过操作,所以默认点就是坐标原点O,结果就是坐标原点O到A(200,200)之间连直线(用蓝色圈1标注)。

    第二次lineTo的时候。因为上次的结束位置是A(200,200),所以就是A(200,200)到B(200,0)之间的连线(用蓝色圈2标注)。

    moveTo 和 setLastPoint:

    方法预览:

            // moveTo
            public void moveTo (float x, float y)
    
            // setLastPoint
            public void setLastPoint (float dx, float dy)

    这两个方法尽管在作用上有类似之处,但实际上却是全然不同的两个东东,具体參照下表:

    方法名 简单介绍 是否影响之前的操作 是否影响之后操作
    moveTo 移动下一次操作的起点位置
    setLastPoint 设置之前操作的最后一个点位置

    废话不多说,直接上代码:

            canvas.translate(mWidth / 2, mHeight / 2);  // 移动坐标系到屏幕中心
    
            Path path = new Path();                     // 创建Path
    
            path.lineTo(200, 200);                      // lineTo
    
            path.moveTo(200,100);                       // moveTo
    
            path.lineTo(200,0);                         // lineTo
    
            canvas.drawPath(path, mPaint);              // 绘制Path

    这个和上面演示lineTo的方法类似,仅仅只是在两个lineTo之间加入了一个moveTo。

    moveTo仅仅改变下次操作的起点。在执行完第一次LineTo的时候,本来的默认点位置是A(200,200),可是moveTo将其改变成为了C(200,100),所以在第二次调用lineTo的时候就是连接C(200,100) 到 B(200,0) 之间的直线(用蓝色圈2标注)。

    以下是setLastPoint的演示样例:

            canvas.translate(mWidth / 2, mHeight / 2);  // 移动坐标系到屏幕中心
    
            Path path = new Path();                     // 创建Path
    
            path.lineTo(200, 200);                      // lineTo
    
            path.setLastPoint(200,100);                 // setLastPoint
    
            path.lineTo(200,0);                         // lineTo
    
            canvas.drawPath(path, mPaint);              // 绘制Path

    setLastPoint是重置上一次操作的最后一个点。在执行完第一次的lineTo的时候,最后一个点是A(200,200),而setLastPoint更改最后一个点为C(200,100),所以在实际执行的时候,第一次的lineTo就不是从原点O到A(200,200)的连线了,而变成了从原点O到C(200,100)之间的连线了。

    在执行完第一次lineTo和setLastPoint后,最后一个点的位置是C(200,100),所以在第二次调用lineTo的时候就是C(200,100) 到 B(200,0) 之间的连线(用蓝色圈2标注)。

    close

    方法预览:

            public void close ()

    close方法用于连接当前最后一个点和最初的一个点(假设两个点不重合的话)。终于形成一个封闭的图形。

            canvas.translate(mWidth / 2, mHeight / 2);  // 移动坐标系到屏幕中心
    
            Path path = new Path();                     // 创建Path
    
            path.lineTo(200, 200);                      // lineTo
    
            path.lineTo(200,0);                         // lineTo
    
            path.close();                               // close
    
            canvas.drawPath(path, mPaint);              // 绘制Path

    非常明显,两个lineTo分别代表第1和第2条线。而close在此处的作用就算连接了B(200,0)点和圆的O之间的第3条线,使之形成一个封闭的图形。

    注意:close的作用是封闭路径,与当前最后一个点和第一个点并不等价。假设连接了最后一个点和第一个点仍然无法形成封闭图形,则close什么 也不做。

    第2组: addXxx与arcTo

    这次内容主要是在Path中加入基本图形,重点区分addArc与arcTo。

    第一类(基本形状)

    方法预览:

    // 第一类(基本形状)
        // 圆形
        public void addCircle (float x, float y, float radius, Path.Direction dir)
        // 椭圆
        public void addOval (RectF oval, Path.Direction dir)
        // 矩形
        public void addRect (float left, float top, float right, float bottom, Path.Direction dir)
        public void addRect (RectF rect, Path.Direction dir)
        // 圆角矩形
        public void addRoundRect (RectF rect, float[] radii, Path.Direction dir)
        public void addRoundRect (RectF rect, float rx, float ry, Path.Direction dir)

    这一类就是在path中加入一个基本形状,基本形状部分和前面所讲的绘制基本形状并无太大差别,详情參考Canvas(1)颜色与基本形状, 本次仅仅将当中不同的部分摘出来具体解说一下。

    细致观察一下第一类是方法,无一例外,在最后都有一个Path.Direction。这是一个什么奇妙的东东?

    Direction的意思是 方向。趋势。 点进去看一下会发现Direction是一个枚举(Enum)类型,里面仅仅有两个枚举常量,例如以下:

    类型 解释 翻译
    CW clockwise 顺时针
    CCW counter-clockwise 逆时针

    瞬间懵逼,我仅仅是想加入一个主要的形状啊,搞什么顺时针和逆时针, (╯‵□′)╯︵┻━┻

    稍安勿躁,┬─┬ ノ( ’ - ‘ノ) {摆好摆好) 既然存在肯定是实用的,先偷偷剧透一下这个顺时针和逆时针的作用。

    序号 作用
    1 在加入图形时确定闭合顺序(各个点的记录顺序)
    2 对图形的渲染结果有影响(是推断图形渲染的重要条件)

    这个先剧透这么多,至于对闭合顺序有啥影响,自相交图形的渲染等问题等请慢慢看下去

    咱们先研究确定闭合顺序的问题。加入一个矩形试试看:

            canvas.translate(mWidth / 2, mHeight / 2);  // 移动坐标系到屏幕中心
    
            Path path = new Path();
    
            path.addRect(-200,-200,200,200, Path.Direction.CW);
    
            canvas.drawPath(path,mPaint);

    将上面代码的CW改为CCW再执行一次。接下来就是见证奇迹的时刻,两次执行结果一模一样,有木有非常奇妙!

    **(╯°Д°)╯︵ ┻━┻(再TM掀一次)
    坑人也不带这种啊,一毛一样要它干嘛。

    **

    事实上啊,这个东东是自带隐身技能的,想要让它现出原形。就要用到咱们刚刚学到的setLastPoint(重置当前最后一个点的位置)。

            canvas.translate(mWidth / 2, mHeight / 2);  // 移动坐标系到屏幕中心
    
            Path path = new Path();
    
            path.addRect(-200,-200,200,200, Path.Direction.CW);
    
            path.setLastPoint(-300,300);                // <-- 重置最后一个点的位置
    
            canvas.drawPath(path,mPaint);

    能够明显看到,图形发生了奇怪的变化。为何会如此呢?

    我们先分析一下。绘制一个矩形(仅绘制边线),实际上仅仅须要进行四次lineTo操作即可了,也就是说。仅仅须要知道4个点的坐标,然后使用moveTo到第一个点,之后依次lineTo即可了(从以上測试能够看出,在实际绘制中也确实是这么干的)。

    可是为什么要这么做呢?确定一个矩形最少须要两个点(对角线的两个点),依据这两个点的坐标直接算出四条边然后画出来不即可了,干嘛还要先计算出四个点坐标,之后再连直线呢?

    这个就要涉及一些path的存储问题了。前面在path中的定义中说过。Path是封装了由直线和曲线(二次,三次贝塞尔曲线)构成的几何路径。当中曲线部分用的是贝塞尔曲线,稍后再讲。

    然而除了曲线部分就仅仅剩下直线了。对于直线的存储最简单的就是记录坐标点,然后直接连接各个点即可了。

    尽管记录矩形仅仅须要两个点,可是假设仅仅用两个点来记录一个矩形的话。就要额外添加一个标志位来记录这是一个矩形,显然对于存储和解析都是非常不划算的事情。将矩形转换为直线,为的就是存储记录方便。

    扯了这么多,该回归正题了,就是我们的顺时针和逆时针在这里是干啥的?

    图形在实际记录中就是记录各个的点。对于一个图形来说肯定有多个点。既然有这么多的点。肯定就须要一个先后顺序。这里顺时针和逆时针就是用来确定记录这些点的顺序的。

    对于上面这个矩形来说。我们採用的是顺时针(CW)。所以记录的点的顺序就是 A -> B -> C -> D. 最后一个点就是D。我们这里使用setLastPoint改变最后一个点的位置实际上是改变了D的位置。

    理解了上面的原理之后。设想假设我们将顺时针改为逆时针(CCW)。则记录点的顺序应该就是 A -> D -> C -> B, 再使用setLastPoint则改变的是B的位置,我们试试看结果和我们的猜想是否一致:

            canvas.translate(mWidth / 2, mHeight / 2);  // 移动坐标系到屏幕中心
    
            Path path = new Path();
    
            path.addRect(-200,-200,200,200, Path.Direction.CCW);
    
            path.setLastPoint(-300,300);                // <-- 重置最后一个点的位置
    
            canvas.drawPath(path,mPaint);

    通过验证发现,发现结果和我们猜想的一样。可是另一个潜藏的问题不晓得大家可否注意到。

    我们用两个点的坐标确定了一个矩形,矩形起始点(A)就是我们指定的第一个点的坐标。

    须要注意的是,交换坐标点的顺序可能就会影响到某些绘制内容哦,比如上面的样例,你能够尝试交换两个坐标点。或者指定另外两个点来作为參数,尽管指定的是同一个矩形,但实际绘制出来是不同的哦。

    參数中点的顺序非常重要!
    參数中点的顺序非常重要!
    參数中点的顺序非常重要!

    重要的话说三遍。本次是用矩形作为样例的,其它的几个图形基本上都包括了曲线,详情參见兴许的贝塞尔曲线部分。

    关于顺时针和逆时针对图形填充结果的影响请等待兴许文章,尽管仅仅讲了一个Path,但也是内容颇多,放进一篇中就太长了,请见谅。

    第二类(Path)

    方法预览:

    // 第二类(Path)
        // path
        public void addPath (Path src)
        public void addPath (Path src, float dx, float dy)
        public void addPath (Path src, Matrix matrix)

    这个相对照较简单。也非常easy理解,就是将两个Path合并成为一个。

    第三个方法是将src加入到当前path之前先使用Matrix进行变换。

    第二个方法比第一个方法多出来的两个參数是将src进行了位移之后再加入进当前path中。

    演示样例:

            canvas.translate(mWidth / 2, mHeight / 2);  // 移动坐标系到屏幕中心
            canvas.scale(1,-1);                         // <-- 注意 翻转y坐标轴
    
            Path path = new Path();
            Path src = new Path();
    
            path.addRect(-200,-200,200,200, Path.Direction.CW);
            src.addCircle(0,0,100, Path.Direction.CW);
    
            path.addPath(src,0,200);
    
            mPaint.setColor(Color.BLACK);           // 绘制合并后的路径
            canvas.drawPath(path,mPaint);

    首先我们新建地方两个Path(矩形和圆形)中心都是坐标原点,我们在将包括圆形的path加入到包括矩形的path之前将其进行移动了一段距离,终于绘制出来的效果就如上面所看到的。

    第三类(addArc与arcTo)

    方法预览:

    // 第三类(addArc与arcTo)
        // addArc
        public void addArc (RectF oval, float startAngle, float sweepAngle)
        // arcTo
        public void arcTo (RectF oval, float startAngle, float sweepAngle)
        public void arcTo (RectF oval, float startAngle, float sweepAngle, boolean forceMoveTo)

    从名字就能够看出,这两个方法都是与圆弧相关的,作用都是加入一个圆弧到path中,但既然存在两个方法,两者之间肯定是有差别的:

    名称 作用 差别
    addArc 加入一个圆弧到path 直接加入一个圆弧到path中
    arcTo 加入一个圆弧到path 加入一个圆弧到path。假设圆弧的起点和上次最后一个坐标点不同样。就连接两个点

    能够看到addArc有1个方法(实际上是两个的,但另一个重载方法是API21加入的), 而arcTo有2个方法,当中一个最后多了一个布尔类型的变量forceMoveTo。

    forceMoveTo是什么作用呢?

    这个变量意思为“是否强制使用moveTo”,也就是说,是否使用moveTo将变量移动到圆弧的起点位移,也就意味着:

    forceMoveTo 含义 等价方法
    true 将最后一个点移动到圆弧起点,即不连接最后一个点与圆弧起点 public void addArc (RectF oval, float startAngle, float sweepAngle)
    false 不移动,而是连接最后一个点与圆弧起点 public void arcTo (RectF oval, float startAngle, float sweepAngle)

    演示样例(addArc):

            canvas.translate(mWidth / 2, mHeight / 2);  // 移动坐标系到屏幕中心
            canvas.scale(1,-1);                         // <-- 注意 翻转y坐标轴
    
            Path path = new Path();
            path.lineTo(100,100);
    
            RectF oval = new RectF(0,0,300,300);
    
            path.addArc(oval,0,270);
            // path.arcTo(oval,0,270,true);             // <-- 和上面一句作用等价
    
            canvas.drawPath(path,mPaint);

    演示样例(arcTo):

            canvas.translate(mWidth / 2, mHeight / 2);  // 移动坐标系到屏幕中心
            canvas.scale(1,-1);                         // <-- 注意 翻转y坐标轴
    
            Path path = new Path();
            path.lineTo(100,100);
    
            RectF oval = new RectF(0,0,300,300);
    
            path.arcTo(oval,0,270);
            // path.arcTo(oval,0,270,false);             // <-- 和上面一句作用等价
    
            canvas.drawPath(path,mPaint);

    从上面两张执行效果图能够清晰的看出来两者的差别。我就不再废话了。

    第3组:isEmpty、 isRect、isConvex、 set 和 offset

    这一组比較简单,略微说一下就能够了。

    isEmpty

    方法预览:

            public boolean isEmpty ()

    推断path中是否包括内容。

            Path path = new Path();
            Log.e("1",path.isEmpty()+"");
    
            path.lineTo(100,100);
            Log.e("2",path.isEmpty()+"");

    log输出结果:

    03-02 14:22:54.770 12379-12379/com.sloop.canvas E/1: true
    03-02 14:22:54.770 12379-12379/com.sloop.canvas E/2: false

    isRect

    方法预览:

    public boolean isRect (RectF rect)

    推断path是否是一个矩形,假设是一个矩形的话。会将矩形的信息存放进參数rect中。

            path.lineTo(0,400);
            path.lineTo(400,400);
            path.lineTo(400,0);
            path.lineTo(0,0);
    
            RectF rect = new RectF();
            boolean b = path.isRect(rect);
            Log.e("Rect","isRect:"+b+"| left:"+rect.left+"| top:"+rect.top+"| right:"+rect.right+"| bottom:"+rect.bottom);

    log 输出结果:

    03-02 16:48:39.669 24179-24179/com.sloop.canvas E/Rect: isRect:true| left:0.0| top:0.0| right:400.0| bottom:400.0

    set

    方法预览:

            public void set (Path src)

    将新的path赋值到现有path。

            canvas.translate(mWidth / 2, mHeight / 2);  // 移动坐标系到屏幕中心
            canvas.scale(1,-1);                         // <-- 注意 翻转y坐标轴
    
            Path path = new Path();                     // path加入一个矩形
            path.addRect(-200,-200,200,200, Path.Direction.CW);
    
            Path src = new Path();                      // src加入一个圆
            src.addCircle(0,0,100, Path.Direction.CW);
    
            path.set(src);                              // 大致相当于 path = src;
    
            canvas.drawPath(path,mPaint);

    offset

    方法预览:

            public void offset (float dx, float dy)
            public void offset (float dx, float dy, Path dst)

    这个的作用也非常简单,就是对path进行一段平移,它和Canvas中的translate作用非常像。但Canvas作用于整个画布,而path的offset仅仅作用于当前path。

    可是第二个方法最后怎么会有一个path作为參数?

    事实上第二个方法中最后的參数das是存储平移后的path的。

    dst状态 效果
    dst不为空 将当前path平移后的状态存入dst中,不会影响当前path
    dat为空(null) 平移将作用于当前path,相当于第一种方法

    演示样例:

            canvas.translate(mWidth / 2, mHeight / 2);  // 移动坐标系到屏幕中心
            canvas.scale(1,-1);                         // <-- 注意 翻转y坐标轴
    
            Path path = new Path();                     // path中加入一个圆形(圆心在坐标原点)
            path.addCircle(0,0,100, Path.Direction.CW);
    
            Path dst = new Path();                      // dst中加入一个矩形
            dst.addRect(-200,-200,200,200, Path.Direction.CW);
    
            path.offset(300,0,dst);                     // 平移
    
            canvas.drawPath(path,mPaint);               // 绘制path
    
            mPaint.setColor(Color.BLUE);                // 更改画笔颜色
    
            canvas.drawPath(dst,mPaint);                // 绘制dst

    从执行效果图能够看出。尽管我们在dst中加入了一个矩形,可是并没有表现出来,所以,当dst中存在内容时,dst中原有的内容会被清空。而存放平移后的path。

    三.总结

    本想一篇把path写完,可是万万没想到竟然扯了这么多。本篇中解说的是直线部分和一些经常用法。下一篇将着重解说贝塞尔曲线和自相交图形渲染等相关问题,敬请期待哦。

    学完本篇之后又解锁了新的境地,能够看看这位大神的文章 Android雷达图(蜘蛛网图)绘制

    这个精小干练,非常适合新手练习使用,帮助大家更好的熟悉path的使用。

    (,,• ₃ •,,)

    PS: 因为本人水平有限,某些地方可能存在误解或不准确。假设你对此有疑问能够提交Issues进行反馈。

    About Me

    作者微博: @GcsSloop

    參考资料

    Path

    Canvas

    android画图之Path总结

  • 相关阅读:
    vim:spell语法
    ubuntu安装texlive2019
    virtualbox安装ubuntu
    正在阅读的tex教程
    Koa 框架介绍以及 Koa2.x 环境搭建
    检测Android应用的通知栏权限开启状态(已经适配8.0以上系统)
    Redis 的 8 大应用场景
    Redis问与答
    SpringBoot中使用Redis
    Mac环境下安装Redis
  • 原文地址:https://www.cnblogs.com/llguanli/p/9890384.html
Copyright © 2011-2022 走看看