• Android 正 N 边形圆角头像的实现


    卖一下广告,欢迎大家关注我的微信公众号,扫一扫下方二维码或搜索微信号 stormjun94(徐公码字),即可关注。 目前专注于 Android 开发,主要分享 Android开发相关知识和一些相关的优秀文章,包括个人总结,职场经验等。

    image

    前言

    在上一篇博客 Android 圆形头像的两种实现方式 中,我们塔伦了实现圆形头像的两种实现方式。

    • 第一种: 使用 Paint 的 Xfermode 实战
    • 第二种: 使用 BitmapShader 实现

    今天,让我们一起来看一下怎样实现正 N 变形圆角头像的实现。

    在讲解之前,让我们先来看一下怎样使用我们的控件

    老规矩,在讲解怎样实现以前,我们先一起来看一下怎样使用我们的自定义控件。

    自定义属性说明

    <attr name="type">
        <enum name="circle" value="0" />
        <enum name="round" value="1" />
        <enum name="polygon" value="2"/>
    </attr>
    
    <declare-styleable name="MultiImageView">
        <attr name="type"/>
        <attr name="miv_border_width" format="dimension" />
        <attr name="miv_border_color" format="color" />
        <attr name="miv_border_overlay" format="boolean" />
        <attr name="miv_fill_color" format="color" />
        <attr name="miv_corner_radius" format="dimension"/>
        <attr name="miv_sides" format="integer"/>
        <attr name="miv_rotate_angle" format="float"/>
    </declare-styleable>
    
    参数 说明
    type 相应的值有 circle,round,polygon
    miv_border_width 表示边界 Path 的宽度 (默认值是 0 )
    miv_border_color 表示边界 Path 的 Color
    miv_border_overlay 表示边界 Path 是否要覆盖在图片上面
    miv_fill_color 表示填充圆的颜色,默认是 Translate,即不可见
    miv_corner_radius 只有当 type round 或者 polygon 的时候才生效,表示边界 Path 圆角半径的大小,
    miv_sides 正 N 边形的变数,只有 type 为 polygon 的时候,该属性才生效
    miv_rotate_angle 旋转的角度,只有 type 为 polygon 的时候,该属性才生效

    指定圆形头像

    <com.xj.shapeview.MultiImageView
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:src="@mipmap/tanyan"
        app:type="circle"
    />
    

    image

    指定圆角矩形

    <com.xj.shapeview.MultiImageView
        android:layout_marginLeft="15dp"
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:src="@mipmap/tanyan"
        app:type="round"
        app:miv_corner_radius="15dp"/>
        
    

    image

    指定正 N 边形

    正五边形

    <com.xj.shapeview.MultiImageView
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:src="@mipmap/tanyan"
        app:type="polygon"
        app:miv_sides="5"
        app:miv_corner_radius="25dp"/>
    

    image

    如果需要其旋转相应的角度,我们只需指定 app:miv_rotate_angle="180" 即可,这里以 180 度为列子讲解说明

    image

    如果需要正六边形,只需要更改为 app:miv_sides="6"

    image

    效果图

    image

    相应的布局文件实现

    <?xml version="1.0" encoding="utf-8"?>
    
    <ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
                xmlns:app="http://schemas.android.com/apk/res-auto"
                xmlns:tools="http://schemas.android.com/tools"
                android:layout_width="match_parent"
                android:layout_height="match_parent">
    
    
        <GridLayout
            android:columnCount="3"
            android:rowCount="3"
    
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="vertical"
            >
    
    
            <com.xj.shapeview.MultiImageView
                android:layout_width="100dp"
                android:layout_height="100dp"
                android:src="@mipmap/tanyan"
                app:type="circle"
                app:miv_sides="6"
                app:miv_corner_radius="15dp"/>
    
            <com.xj.shapeview.MultiImageView
                android:layout_marginLeft="15dp"
                android:layout_width="100dp"
                android:layout_height="100dp"
                android:src="@mipmap/tanyan"
                app:miv_sides="5"
                app:type="round"
                app:miv_corner_radius="15dp"/>
    
            <com.xj.shapeview.MultiImageView
                android:layout_width="100dp"
                android:layout_height="100dp"
                android:src="@mipmap/tanyan"
                app:type="polygon"
                app:miv_sides="5"
                app:miv_corner_radius="25dp"/>
    
            <com.xj.shapeview.MultiImageView
                android:layout_width="100dp"
                android:layout_height="100dp"
                android:src="@mipmap/tanyan"
                app:type="polygon"
                app:miv_sides="5"
    
                app:miv_corner_radius="25dp"
                app:miv_rotate_angle="180"/>
    
            <com.xj.shapeview.MultiImageView
                android:layout_width="100dp"
                android:layout_height="100dp"
                android:src="@mipmap/tanyan"
                app:miv_sides="7"
                app:type="polygon"
                app:miv_corner_radius="0dp"
                app:miv_rotate_angle="0"/>
    
    
            <com.xj.shapeview.MultiImageView
                android:layout_width="100dp"
                android:layout_height="100dp"
                android:src="@mipmap/tanyan"
                app:miv_sides="6"
                app:type="polygon"
                app:miv_corner_radius="0dp"
                app:miv_border_overlay="true"
                 app:miv_fill_color="@color/colorAccent"/>
    
            <com.xj.shapeview.MultiImageView
                android:layout_width="100dp"
                android:layout_height="100dp"
                android:src="@mipmap/tanyan"
                app:miv_sides="6"
                app:type="polygon"
                app:miv_corner_radius="0dp"
                app:miv_rotate_angle="0"
                app:miv_border_overlay="true"
                app:miv_border_width="1dp"
                app:miv_border_color="@android:color/darker_gray"
              />
    
    
            <com.xj.shapeview.MultiImageView
                android:layout_width="100dp"
                android:layout_height="100dp"
                android:src="@mipmap/tanyan"
                app:miv_sides="6"
                app:type="polygon"
                app:miv_corner_radius="0dp"
                app:miv_rotate_angle="0"
                app:miv_border_overlay="false"
                app:miv_border_width="1dp"
                app:miv_border_color="@android:color/black"
            />
    
    
    
            <com.xj.shapeview.MultiImageView
                android:layout_width="100dp"
                android:layout_height="100dp"
                android:src="@mipmap/tanyan"
                app:miv_sides="7"
                app:type="polygon"
                app:miv_corner_radius="10dp"
                app:miv_rotate_angle="0"
    
            />
    
    
        </GridLayout>
    </ScrollView>
    

    正 N 边形圆角头像的实现原理分析

    要实现正 N 变形主要有几个难点

    • 怎样让我们的头像变成正 N 边形
    • 怎样绘制正 N 边形
    • 怎样绘制带圆角的正 N 边形

    怎样让我们的头像变成正 N 边形?

    其实这个问题在上篇博客已经讲到,有两种实现方式。

    • 第一种: 使用 Paint 的 Xfermode 实战
    • 第二种: 使用 BitmapShader 实现

    今天,这边博客主要以 BitmapShader 为例子实现。

    核心代码实现

    mBitmapShader = new BitmapShader(mBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
    mBitmapPaint.setAntiAlias(true);
    mBitmapPaint.setShader(mBitmapShader);
    
    @Override
    protected void onDraw(Canvas canvas) {
    
           -----
    
        Path path = getPath(canvas,mType,(int)mDrawableRadius*2,(int)mDrawableRadius*2,mDrawableRadius,mSides,mCornerRadius);
    
        canvas.drawPath(path,mBitmapPaint);
    }
    

    核心思路分析:

    • 拿到 Bitmap,并使用 BitmapShader 进行包装
    • 将 mBitmapShader 设置给画笔 Paint
    • 第三步,在 onDraw 方法,将其绘制出来

    怎样绘制正 N 边形

    这里的思想主要来自该博客 如何用Canvas画一个正多边形

    数学原理分析

    首先,我们先来看一张图片

    image

    从图中可以看一看到,我们若想绘制出一个正 N 边形,那么我们只需要计算出各个点的坐标,然后使用 Path 连接起来即可。

    那我们要怎样计算出各个点的坐标呢

    • 从图中不难得出,圆心角 a 的度数为 360/n,弧度计算为 2π/n
    • 如果把圆心的坐标为(0,0),那么顶点P1的坐标为[X1=cos(a),Y1=sin(a)]。
    • 以此类推,顶点Pn坐标为[Xn=cos(an),Yn=sin(an)]。
      圆心的实际坐标是外接矩形的中心:[Ox=(rect.right+rect.left)/2 , Oy=(rect.top+rect.bottom)/2]。
      所以Pn的实际坐标是[Xn+Ox,Yn+Oy]。

    最后我们把把 P0-P1…Pn 连起来,就是我们要的结果了。

    核心伪代码实现

    float a = 2π / n ; // 角度
    Path path = new Path();
    for( int i = 0; i < = n; i++ ){
        float x = R * cos(a * i); 
        float y = R * sin(a * i);
        if (i = 0){
            path.moveTo(x,y); // 移动到第一个顶点   
        }else{
            path.lineTo(x,y); //    
        }
    }
    drawPath(path);
    

    实际代码实现

    在上面的例子中,我们假设我们的圆形坐标是 (0,0), 但实际上并不是,实际上在 Android 中我们的圆心坐标是 (width/2,height/2)。因此,我们在计算坐标的时候需要加上

    圆心坐标
    float mX = (rect.right + rect.left) / 2;
    float my = (rect.top + rect.bottom) / 2;
    
    // PN点的 x,y 坐标
    float nextX = mX + Double.valueOf(r * Math.cos(alpha)).floatValue();
    float nextY = my + Double.valueOf(r * Math.sin(alpha)).floatValue();
    
    

    当然我们这里以可以用 canvas 的 translate 方法来移动。

    public static void drawPolygon (RectF rect, Canvas canvas, Paint paintByLevel, int number) {
        if(number < 3) {
            return;
        }
        float r = (rect.right - rect.left) / 2;
        float mX = (rect.right + rect.left) / 2;
        float my = (rect.top + rect.bottom) / 2;
        Path path = new Path();
        for (int i = 0; i <= number; i++) {
            // - 0.5 : Turn 90 ° counterclockwise
            float alpha = Double.valueOf(((2f / number) * i - 0.5) * Math.PI).floatValue();
            float nextX = mX + Double.valueOf(r * Math.cos(alpha)).floatValue();
            float nextY = my + Double.valueOf(r * Math.sin(alpha)).floatValue();
            if (i == 0) {
                path.moveTo(nextX, nextY);
            } else {
                path.lineTo(nextX, nextY);
            }
        }
        canvas.drawPath(path, paintByLevel);
    }
    

    怎样绘制带有圆角的正 N 边形

    这个问题我一开始的思路是根据圆形的半径,然后计算出各个点的坐标,接着使用 path 中的 addArc() 方法来绘制。但是在计算各个点的坐标的时候,遇到很多难度,最后无法得出。

    后面查阅了 Android 官方的文档,发现了有这样一个方法

    PathEffect setPathEffect (PathEffect effect)

    从字面意思很容易理解,就是设置 PathEffect,可以对 Path 产生相应的影响。

    那这个 PathEffect 又是什么东东呢?

    public class PathEffect extends Object

    Known Direct Subclasses
    ComposePathEffect,CornerPathEffect,DashPathEffect,DiscretePathEffect,PathDashPathEffect,SumPathEffect

    从官方文档可以了解到是继承于 Object 的,实现的子类有 ComposePathEffect, CornerPathEffect, DashPathEffect 等。

    看到这里的时候你有没有突然有一种醍醐灌顶的感觉? 这个 CornerPathEffect 是不是就可以实现呢?没错,确实可以实现,而且贼简单。

    核心代码只有这几句,就可以让我们绘制出的正 N 边形具有圆角

    CornerPathEffect cornerPathEffect = new CornerPathEffect(mCornerRadius);
    mBitmapPaint.setPathEffect(cornerPathEffect);
    

    代码实现细节注意事项

    当空间的宽度和高度不一致的时候,半径怎样取值?

    这里我们选择宽度和高度值较小的一个,然后除以2

    mDrawableRadius = Math.min(mDrawableRect.height() / 2.0f, mDrawableRect.width() / 2.0f);
    

    当图片较大的时候,会不会发生 OOM

    当图片较大的时候,我们会对其进行相应的缩放,采用的是矩阵的方法

    private void updateShaderMatrix() {
        float scale;
        float dx = 0;
        float dy = 0;
    
        mShaderMatrix.set(null);
    
        if (mBitmapWidth * mDrawableRect.height() > mDrawableRect.width() * mBitmapHeight) {
            scale = mDrawableRect.height() / (float) mBitmapHeight;
            dx = (mDrawableRect.width() - mBitmapWidth * scale) * 0.5f;
        } else {
            scale = mDrawableRect.width() / (float) mBitmapWidth;
            dy = (mDrawableRect.height() - mBitmapHeight * scale) * 0.5f;
        }
    
        mShaderMatrix.setScale(scale, scale);
        mShaderMatrix.postTranslate((int) (dx + 0.5f) + mDrawableRect.left, (int) (dy + 0.5f) + mDrawableRect.top);
    
    
        mBitmapShader.setLocalMatrix(mShaderMatrix);
    }
    

    自定义控件怎样支持 padding 属性

    在绘制图片的时候,我们对其进行相应的处理,确保我们的坐标是正确的。

    float left = getPaddingLeft() + (availableWidth - sideLength) / 2f;
    float top = getPaddingTop() + (availableHeight - sideLength) / 2f;
    
    case CIRCLY:
        ClipHelper.setCirclePath(path,width,height);
        break;
    case RECTAHGE:
        ClipHelper.setRectangle(path,calculateBounds(),cornerRadius);
        break;
    case POLYGON:
        ClipHelper.setPolygon(path,calculateBounds(),sides,mRotateAngles);
        break;
    
    
    
    private RectF calculateBounds() {
        int availableWidth  = getWidth() - getPaddingLeft() - getPaddingRight();
        int availableHeight = getHeight() - getPaddingTop() - getPaddingBottom();
    
        int sideLength = Math.min(availableWidth, availableHeight);
    
        float left = getPaddingLeft() + (availableWidth - sideLength) / 2f;
        float top = getPaddingTop() + (availableHeight - sideLength) / 2f;
    
        return new RectF(left, top, left + sideLength, top + sideLength);
    }
    

    正 N 边形的角度旋转是怎样实现的。

    其实,这里,我们采用的是矩阵的方式进行旋转的,调用 path.transform 方法

    Matrix matrix = new Matrix();
    matrix.postRotate(rotateAngle,mX,my);
    path.transform(matrix);
    

    题外话

    在开发的时候,一刚开始说要实现圆角六边形的时候,查阅了相关的资料,知道有两种方法

    • 第一种方法,让 UI 设计师直接给图, 使用 Paint 的 Xfermode 实现
    • 第二种方法:直接绘制 Path;

    那时候项目比较赶,采用的是第一种方式实现。不过作为一名程序猿,感觉采用第一种方法实现,总感觉有点 low。后面晚上下班的时候,查阅了相关的资料,最终终于实现了上述的效果。

    这种正 N 边形圆角头像的效果,说难也不难,说容易也不容易。因为里面综合了很多知识点,需要一步步去处理。(比如怎样绘制正 N 边形,怎样支持圆角,怎样处理 Padding 等等)。

    最后,给大家推荐 github 上面的一个开源库。ShapeOfView,里面实现了很多常见的图片(心形,五角星。六角形等)


    参考博客:如何用Canvas画一个正多边形

    Android 圆形头像的两种实现方式
    Android 正 N 边形圆角头像的实现

    如果,你觉得效果还不错,请到我的 github 上面 star,谢谢。

    MultiImageView

    image

  • 相关阅读:
    fastDFS同步问题讨论
    Android开发(26)--补间动画(Tween)的实现
    android布局
    Linux特殊权限:SUID、SGID、SBIT
    如何使用ssh-keygen生成key
    Linux中环境变量到底写在哪个文件中?解析login shell 和 no-login shell
    使用ssh无密码登录
    github中的ssh配置
    PHP中的一个很好用的文件上传类
    [置顶] js模板方法的思路及实现
  • 原文地址:https://www.cnblogs.com/gdutxiaoxu/p/11594539.html
Copyright © 2020-2023  润新知