zoukankan      html  css  js  c++  java
  • Android BitmapShader 实战 实现圆形、圆角图片

    1、概述

    记得初学那会写过一篇博客Android 完美实现图片圆角和圆形(对实现进行分析),主要是个自定View加上使用Xfermode实现的。其实实现圆角图片的方法应该很多,常见的就是利用Xfermode,Shader。本篇博客会直接继承直接继承ImageView,使用BitmapShader实现圆角的绘制,大家如果耐着性子看完,我估计什么形状都能绘制出来。

    2、效果图

    这是圆角的一个演示图~~这个没什么说的,直接设置的圆角的大小就行;

    这是圆形的显示图,这里需要注意下,因为设置的图片可能是长方形,例如上图:有两个长方形,一个宽比较大,一个高比较大;

    那么我们希望显示成圆形,我们可能就要对其进行放大或者缩小(因为图片的宽可能不满足设置的边长,而高超出,此时我们就需要放大其宽度)。

    这个一张图,中间是正常尺寸;上下分别为特大特小,主要可以当尺寸大于或者小于设置尺寸,我们需要对其放大或者缩小;

    圆角时如果图片与view的宽高不一致,也需要进行放大缩小,这里就不截图了,代码里面看吧。

    3、浅谈BitmapShader

    BitmapShader是Shader的子类,可以通过Paint.setShader(Shader shader)进行设置、

    这里我们只关注BitmapShader,构造方法:

    mBitmapShader = new BitmapShader(bitmap, TileMode.CLAMP, TileMode.CLAMP);

    参数1:bitmap

    参数2,参数3:TileMode;

    TileMode的取值有三种:

    CLAMP 拉伸

    REPEAT 重复

    MIRROR 镜像

    如果大家给电脑屏幕设置屏保的时候,如果图片太小,可以选择重复、拉伸、镜像;

    重复:就是横向、纵向不断重复这个bitmap

    镜像:横向不断翻转重复,纵向不断翻转重复;

    拉伸:这个和电脑屏保的模式应该有些不同,这个拉伸的是图片最后的那一个像素;横向的最后一个横行像素,不断的重复,纵项的那一列像素,不断的重复;

    现在大概明白了,BitmapShader通过设置给mPaint,然后用这个mPaint绘图时,就会根据你设置的TileMode,对绘制区域进行着色。

    这里需要注意一点:就是BitmapShader是从你的画布的左上角开始绘制的,不在view的右下角绘制个正方形,它不会在你正方形的左上角开始。

    好了,到此,我相信大家对BitmapShader有了一定的了解了;当然了,如果你希望对Shader充分的了解,请参考爱歌的神作: 自定义控件其实很简单1/3 。

    对于我们的圆角,以及圆形,我们设置的模式都是CLAMP ,但是你会不会会有一个疑问:

    view的宽或者高大于我们的bitmap宽或者高岂不是会拉伸?

    嗯,我们会为BitmapShader设置一个matrix,去适当的放大或者缩小图片,不会让“ view的宽或者高大于我们的bitmap宽或者高 ”此条件成立的。

    到此我们的原理基本介绍完毕了,拿到drawable转化为bitmap,然后直接初始化BitmapShader,画笔设置Shader,最后在onDraw里面进行画圆就行了。

    4、BitmapShader实战

    首先就来看看利用BitmapShader实现的圆形或者圆角。

    我们这里直接继承ImageView,这样大家设置图片的代码会比较熟悉;但是我们需要支持两种模式,那么就需要自定义属性了:

    1、自定义属性

    values/attr.xml

    1. <?xml version="1.0" encoding="utf-8"?>  
    2. <resources>  
    3.   
    4.     <attr name="borderRadius" format="dimension" />  
    5.     <attr name="type">  
    6.         <enum name="circle" value="0" />  
    7.         <enum name="round" value="1" />  
    8.     </attr>  
    9.     
    10.   
    11.     <declare-styleable name="RoundImageView">  
    12.         <attr name="borderRadius" />  
    13.         <attr name="type" />  
    14.     </declare-styleable>  
    15.   
    16. </resources>  

    我们定义了一个枚举和一个圆角的大小borderRadius。


    2、获取自定义属性

    1. public class RoundImageView extends ImageView  
    2. {  
    3.   
    4.     /** 
    5.      * 图片的类型,圆形or圆角 
    6.      */  
    7.     private int type;  
    8.     private static final int TYPE_CIRCLE = 0;  
    9.     private static final int TYPE_ROUND = 1;  
    10.   
    11.     /** 
    12.      * 圆角大小的默认值 
    13.      */  
    14.     private static final int BODER_RADIUS_DEFAULT = 10;  
    15.     /** 
    16.      * 圆角的大小 
    17.      */  
    18.     private int mBorderRadius;  
    19.   
    20.     /** 
    21.      * 绘图的Paint 
    22.      */  
    23.     private Paint mBitmapPaint;  
    24.     /** 
    25.      * 圆角的半径 
    26.      */  
    27.     private int mRadius;  
    28.     /** 
    29.      * 3x3 矩阵,主要用于缩小放大 
    30.      */  
    31.     private Matrix mMatrix;  
    32.     /** 
    33.      * 渲染图像,使用图像为绘制图形着色 
    34.      */  
    35.     private BitmapShader mBitmapShader;  
    36.     /** 
    37.      * view的宽度 
    38.      */  
    39.     private int mWidth;  
    40.     private RectF mRoundRect;  
    41.   
    42.     public RoundImageView(Context context, AttributeSet attrs)  
    43.     {  
    44.         super(context, attrs);  
    45.         mMatrix = new Matrix();  
    46.         mBitmapPaint = new Paint();  
    47.         mBitmapPaint.setAntiAlias(true);  
    48.   
    49.         TypedArray a = context.obtainStyledAttributes(attrs,  
    50.                 R.styleable.RoundImageView);  
    51.   
    52.         mBorderRadius = a.getDimensionPixelSize(  
    53.                 R.styleable.RoundImageView_borderRadius, (int) TypedValue  
    54.                         .applyDimension(TypedValue.COMPLEX_UNIT_DIP,  
    55.                                 BODER_RADIUS_DEFAULT, getResources()  
    56.                                         .getDisplayMetrics()));// 默认为10dp  
    57.         type = a.getInt(R.styleable.RoundImageView_type, TYPE_CIRCLE);// 默认为Circle  
    58.   
    59.         a.recycle();  
    60.     }  

    可以看到我们的一些成员变量,基本都加了注释;然后在构造方法中获取了我们的自定义属性,以及部分变量的初始化。

    3、onMeasure

    1. @Override  
    2.     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)  
    3.     {  
    4.         Log.e("TAG", "onMeasure");  
    5.         super.onMeasure(widthMeasureSpec, heightMeasureSpec);  
    6.   
    7.         /** 
    8.          * 如果类型是圆形,则强制改变view的宽高一致,以小值为准 
    9.          */  
    10.         if (type == TYPE_CIRCLE)  
    11.         {  
    12.             mWidth = Math.min(getMeasuredWidth(), getMeasuredHeight());  
    13.             mRadius = mWidth / 2;  
    14.             setMeasuredDimension(mWidth, mWidth);  
    15.         }  
    16.   
    17.     }  


    我们复写了onMeasure方法,主要用于当设置类型为圆形时,我们强制让view的宽和高一致。

    接下来只剩下设置BitmapShader和绘制了

    4、设置BitmapShader

    1. /** 
    2.      * 初始化BitmapShader 
    3.      */  
    4.     private void setUpShader()  
    5.     {  
    6.         Drawable drawable = getDrawable();  
    7.         if (drawable == null)  
    8.         {  
    9.             return;  
    10.         }  
    11.   
    12.         Bitmap bmp = drawableToBitamp(drawable);  
    13.         // 将bmp作为着色器,就是在指定区域内绘制bmp  
    14.         mBitmapShader = new BitmapShader(bmp, TileMode.CLAMP, TileMode.CLAMP);  
    15.         float scale = 1.0f;  
    16.         if (type == TYPE_CIRCLE)  
    17.         {  
    18.             // 拿到bitmap宽或高的小值  
    19.             int bSize = Math.min(bmp.getWidth(), bmp.getHeight());  
    20.             scale = mWidth * 1.0f / bSize;  
    21.   
    22.         } else if (type == TYPE_ROUND)  
    23.         {  
    24.             // 如果图片的宽或者高与view的宽高不匹配,计算出需要缩放的比例;缩放后的图片的宽高,一定要大于我们view的宽高;所以我们这里取大值;  
    25.             scale = Math.max(getWidth() * 1.0f / bmp.getWidth(), getHeight()  
    26.                     * 1.0f / bmp.getHeight());  
    27.         }  
    28.         // shader的变换矩阵,我们这里主要用于放大或者缩小  
    29.         mMatrix.setScale(scale, scale);  
    30.         // 设置变换矩阵  
    31.         mBitmapShader.setLocalMatrix(mMatrix);  
    32.         // 设置shader  
    33.         mBitmapPaint.setShader(mBitmapShader);  
    34.     }  

    在setUpShader中,首先对drawable转化为我们的bitmap;

    然后初始化mBitmapShader = new BitmapShader(bmp, TileMode.CLAMP, TileMode.CLAMP);

    接下来,根据类型以及bitmap和view的宽高,计算scale;

    关于scale的计算:

    圆形时:取bitmap的宽或者高的小值作为基准,如果采用大值,缩放后肯定不能填满我们的圆形区域。然后,view的mWidth/bSize ; 得到的就是scale。

    圆角时:因为设计到宽/高比例,我们分别getWidth() * 1.0f / bmp.getWidth() 和 getHeight() * 1.0f / bmp.getHeight() ;最终取大值,因为我们要让最终缩放完成的图片一定要大于我们的view的区域,有点类似centerCrop;

    比如:view的宽高为10*20;图片的宽高为5*100 ; 最终我们应该按照宽的比例放大,而不是按照高的比例缩小;因为我们需要让缩放后的图片,自定大于我们的view宽高,并保证原图比例。

    有了scale,就可以设置给我们的matrix;

    然后使用mBitmapShader.setLocalMatrix(mMatrix);

    最后将bitmapShader设置给paint。

    关于drawable转bitmap的代码:

    1. /** 
    2.      * drawable转bitmap 
    3.      *  
    4.      * @param drawable 
    5.      * @return 
    6.      */  
    7.     private Bitmap drawableToBitamp(Drawable drawable)  
    8.     {  
    9.         if (drawable instanceof BitmapDrawable)  
    10.         {  
    11.             BitmapDrawable bd = (BitmapDrawable) drawable;  
    12.             return bd.getBitmap();  
    13.         }  
    14.         int w = drawable.getIntrinsicWidth();  
    15.         int h = drawable.getIntrinsicHeight();  
    16.         Bitmap bitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);  
    17.         Canvas canvas = new Canvas(bitmap);  
    18.         drawable.setBounds(0, 0, w, h);  
    19.         drawable.draw(canvas);  
    20.         return bitmap;  
    21.     }  

    最后我们会在onDraw里面调用setUpShader(),然后进行绘制。

    5、绘制

    到此,就剩下最后一步绘制了,因为我们的范围,以及缩放都完成了,所以真的只剩下绘制了。

    1. @Override  
    2.     protected void onDraw(Canvas canvas)  
    3.     {  
    4.         if (getDrawable() == null)  
    5.         {  
    6.             return;  
    7.         }  
    8.         setUpShader();  
    9.   
    10.         if (type == TYPE_ROUND)  
    11.         {  
    12.             canvas.drawRoundRect(mRoundRect, mBorderRadius, mBorderRadius,  
    13.                     mBitmapPaint);  
    14.         } else  
    15.         {  
    16.             canvas.drawCircle(mRadius, mRadius, mRadius, mBitmapPaint);  
    17.             // drawSomeThing(canvas);  
    18.         }  
    19.     }  
    20.       
    21.     @Override  
    22.     protected void onSizeChanged(int w, int h, int oldw, int oldh)  
    23.     {  
    24.         super.onSizeChanged(w, h, oldw, oldh);  
    25.         // 圆角图片的范围  
    26.         if (type == TYPE_ROUND)  
    27.             mRoundRect = new RectF(0, 0, getWidth(), getHeight());  
    28.     }  


    绘制就很简单了,画个圆,圆角矩形什么的。圆角矩形的限定范围mRoundRect在onSizeChanged里面进行了初始化。

    5、状态的存储与恢复

    当然了,如果内存不足,而恰好我们的Activity置于后台,不幸被重启,或者用户旋转屏幕造成Activity重启,我们的View应该也能尽可能的去保存自己的属性。
    状态保存什么用处呢?比如,现在一个的圆角大小是10dp,用户点击后变成50dp;当用户旋转以后,或者长时间置于后台以后,返回我们的Activity应该还是50dp;
    我们简单的存储一下,当前的type以及mBorderRadius
    1. private static final String STATE_INSTANCE = "state_instance";  
    2.     private static final String STATE_TYPE = "state_type";  
    3.     private static final String STATE_BORDER_RADIUS = "state_border_radius";  
    4.   
    5.     @Override  
    6.     protected Parcelable onSaveInstanceState()  
    7.     {  
    8.         Bundle bundle = new Bundle();  
    9.         bundle.putParcelable(STATE_INSTANCE, super.onSaveInstanceState());  
    10.         bundle.putInt(STATE_TYPE, type);  
    11.         bundle.putInt(STATE_BORDER_RADIUS, mBorderRadius);  
    12.         return bundle;  
    13.     }  
    14.   
    15.     @Override  
    16.     protected void onRestoreInstanceState(Parcelable state)  
    17.     {  
    18.         if (state instanceof Bundle)  
    19.         {  
    20.             Bundle bundle = (Bundle) state;  
    21.             super.onRestoreInstanceState(((Bundle) state)  
    22.                     .getParcelable(STATE_INSTANCE));  
    23.             this.type = bundle.getInt(STATE_TYPE);  
    24.             this.mBorderRadius = bundle.getInt(STATE_BORDER_RADIUS);  
    25.         } else  
    26.         {  
    27.             super.onRestoreInstanceState(state);  
    28.         }  
    29.   
    30.     }  

    代码比较简单。我们文章中的demo中,第一个,第四个是可以点击的,点击后会发生变化,你可以点击后,然后旋转屏幕进行测试。
     
    同时我们也对外公布了两个方法,用于动态修改圆角大小和type
    1. public void setBorderRadius(int borderRadius)  
    2.     {  
    3.         int pxVal = dp2px(borderRadius);  
    4.         if (this.mBorderRadius != pxVal)  
    5.         {  
    6.             this.mBorderRadius = pxVal;  
    7.             invalidate();  
    8.         }  
    9.     }  
    10.   
    11.     public void setType(int type)  
    12.     {  
    13.         if (this.type != type)  
    14.         {  
    15.             this.type = type;  
    16.             if (this.type != TYPE_ROUND && this.type != TYPE_CIRCLE)  
    17.             {  
    18.                 this.type = TYPE_CIRCLE;  
    19.             }  
    20.             requestLayout();  
    21.         }  
    22.   
    23.     }  
    24.   
    25.     public int dp2px(int dpVal)  
    26.     {  
    27.         return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,  
    28.                 dpVal, getResources().getDisplayMetrics());  
    29.     }  

    最后贴一下我们的布局文件和MainActivity。

    6、调用

    布局文件:
    1. <ScrollView xmlns:android="http://schemas.android.com/apk/res/android"  
    2.     xmlns:tools="http://schemas.android.com/tools"  
    3.     xmlns:zhy="http://schemas.android.com/apk/res/com.zhy.variousshapeimageview"  
    4.     android:layout_width="match_parent"  
    5.     android:layout_height="wrap_content" >  
    6.   
    7.     <LinearLayout  
    8.         android:layout_width="match_parent"  
    9.         android:layout_height="match_parent"  
    10.         android:orientation="vertical" >  
    11.   
    12.         <com.zhy.view.RoundImageView  
    13.             android:id="@+id/id_qiqiu"  
    14.             android:layout_width="wrap_content"  
    15.             android:layout_height="wrap_content"  
    16.             android:layout_margin="10dp"  
    17.             android:src="@drawable/qiqiu" >  
    18.         </com.zhy.view.RoundImageView>  
    19.   
    20.         <com.zhy.view.RoundImageView  
    21.             android:layout_width="200dp"  
    22.             android:layout_height="200dp"  
    23.             android:layout_margin="10dp"  
    24.             android:src="@drawable/aa" >  
    25.         </com.zhy.view.RoundImageView>  
    26.   
    27.         <com.zhy.view.RoundImageView  
    28.             android:layout_width="wrap_content"  
    29.             android:layout_height="wrap_content"  
    30.             android:layout_margin="10dp"  
    31.             android:src="@drawable/icon" >  
    32.         </com.zhy.view.RoundImageView>  
    33.   
    34.         <com.zhy.view.RoundImageView  
    35.             android:id="@+id/id_meinv"  
    36.             android:layout_width="wrap_content"  
    37.             android:layout_height="wrap_content"  
    38.             android:layout_margin="10dp"  
    39.             android:src="@drawable/aa"  
    40.             zhy:borderRadius="20dp"  
    41.             zhy:type="round" >  
    42.         </com.zhy.view.RoundImageView>  
    43.   
    44.         <com.zhy.view.RoundImageView  
    45.             android:layout_width="wrap_content"  
    46.             android:layout_height="wrap_content"  
    47.             android:layout_margin="10dp"  
    48.             android:src="@drawable/icon"  
    49.             zhy:borderRadius="40dp"  
    50.             zhy:type="round" >  
    51.         </com.zhy.view.RoundImageView>  
    52.   
    53.         <com.zhy.view.RoundImageView  
    54.             android:layout_width="wrap_content"  
    55.             android:layout_height="wrap_content"  
    56.             android:layout_margin="10dp"  
    57.             android:src="@drawable/qiqiu"  
    58.             zhy:borderRadius="60dp"  
    59.             zhy:type="round" >  
    60.         </com.zhy.view.RoundImageView>  
    61.     </LinearLayout>  
    62.   
    63. </ScrollView>  
     
    没撒,ScrollView里面一个线性布局,里面一堆RoundImageView。

    MainActivity
    1. package com.zhy.variousshapeimageview;  
    2.   
    3. import android.app.Activity;  
    4. import android.os.Bundle;  
    5. import android.view.View;  
    6. import android.view.View.OnClickListener;  
    7.   
    8. import com.zhy.view.RoundImageView;  
    9.   
    10. public class MainActivity extends Activity  
    11. {  
    12.     private RoundImageView mQiQiu;  
    13.     private RoundImageView mMeiNv ;   
    14.   
    15.     @Override  
    16.     protected void onCreate(Bundle savedInstanceState)  
    17.     {  
    18.         super.onCreate(savedInstanceState);  
    19.         setContentView(R.layout.activity_main);  
    20.           
    21.         mQiQiu = (RoundImageView) findViewById(R.id.id_qiqiu);  
    22.         mMeiNv = (RoundImageView) findViewById(R.id.id_meinv);  
    23.           
    24.         mQiQiu.setOnClickListener(new OnClickListener()  
    25.         {  
    26.             @Override  
    27.             public void onClick(View v)  
    28.             {  
    29.                 mQiQiu.setType(RoundImageView.TYPE_ROUND);  
    30.             }  
    31.         });  
    32.           
    33.         mMeiNv.setOnClickListener(new OnClickListener()  
    34.         {  
    35.               
    36.             @Override  
    37.             public void onClick(View v)  
    38.             {  
    39.                 mMeiNv.setBorderRadius(90);  
    40.             }  
    41.         });  
    42.     }  
    43.   
    44. }  

    好了,到此本篇博客就结束了。大家可以尝试绘制个五边形或者神马的形状;或者加个边框神马的,相信自己修改应该没问题~~代码可能会存在bug和不足之处,欢迎您的指出,共同进步。

    最后的效果图:

  • 相关阅读:
    正经学C#_循环[do while,while,for]:[c#入门经典]
    Vs 控件错位 右侧资源管理器文件夹点击也不管用,显示异常
    asp.net core 获取当前请求的url
    在实体对象中访问导航属性里的属性值出现异常“There is already an open DataReader associated with this Command which must be
    用orchard core和asp.net core 3.0 快速搭建博客,解决iis 部署https无法登录后台问题
    System.Data.Entity.Core.EntityCommandExecution The data reader is incompatible with the specified
    初探Java设计模式3:行为型模式(策略,观察者等)
    MySQL教程77-CROSS JOIN 交叉连接
    MySQL教程76-HAVING 过滤分组
    MySQL教程75-使用GROUP BY分组查询
  • 原文地址:https://www.cnblogs.com/lianghe01/p/4238908.html
Copyright © 2011-2022 走看看