Android中级——屏幕和绘图

屏幕

  • 屏幕大小:指屏幕对角线长度,单位为寸
  • 分辨率:指屏幕宽高的像素点个数,如720x1280
  • PPI:每英寸像素(Pixels Per Inch),指对角线的像素点数除以屏幕大小,又称屏幕密度DPI(Dots Per Inch)

系统屏幕密度

不同手机的大小和像素密度都不同,为统一,定义了几个标准的DPI值

在这里插入图片描述

独立像素密度dp

相同的像素,在不同密度的屏幕上显示,长度会不同,因为高密度的屏幕包含更多像素点

  • 规定密度为mdpi(即密度值为160)时1px=1dp
  • 故上图各分辨率的换算比例为3:4:6:8:12

单位转换

如下工具类提供了px、dp、sp的互相转换

public class DisplayUtil {
    // dp = 像素/密度
    public static int px2dip(Context context, float pxValue) {
        float scale = context.getResources().getDisplayMetrics().density;
        return (int) (pxValue / scale + 0.5f);
    }

    public static int dip2px(Context context, float dipValue) {
        float scale = context.getResources().getDisplayMetrics().density;
        return (int) (dipValue * scale + 0.5f);
    }

    // sp = 像素/缩放密度
    public static int px2sp(Context context, float pxValue) {
        float fontScale = context.getResources().getDisplayMetrics().scaledDensity;
        return (int) (pxValue / fontScale + 0.5f);
    }

    public static int sp2px(Context context, float spValue) {
        float fontScale = context.getResources().getDisplayMetrics().scaledDensity;
        return (int) (spValue * fontScale + 0.5f);
    }
}

或可以使用TypedValue.applyDimension()方法

public int dp2px(int dp) {
    return (int) TypedValue.applyDimension(
            TypedValue.COMPLEX_UNIT_DIP,
            dp,
            getResources().getDisplayMetrics());
}

public int sp2px(int sp) {
    return (int) TypedValue.applyDimension(
            TypedValue.COMPLEX_UNIT_SP,
            sp,
            getResources().getDisplayMetrics());
}

XML绘图(需放在Drawable)

bitmap

对应BitmapDrawable,如下可将图片并转为Bitmap

<?xml version="1.0" encoding="utf-8"?>
<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
    android:src="@mipmap/ic_launcher" />
  • src:资源id
  • antialias:抗锯齿,使图片变得平滑,但会降低清晰度(可忽略不计),应该开启
  • dither:抖动,可让高质量图片在低质量屏幕保持较好的显示效果,应该开启
  • filter:过滤,当图片尺寸被拉伸/压缩时,可保持较好的显示效果,应该开启
  • gravity:当图片小于容器尺寸时,对图片定位
  • minMap:纹理映射,不常用,默认false
  • tileMode:平铺模式,默认disable关闭,开启后gravity失效,repeat重复平铺,mirror镜面投影平铺,clamp边缘像素扩展平铺

nine-patch

对应NinePatchDrawable,表示.9格式照片,可以自动根据所需宽高缩放且不失真,属性同上

shape

对应GradientDrawable,可实现不同类型的形状,如下实现渐变阴影

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle">
    <gradient
        android:angle="45"
        android:endColor="#805FBBFF"
        android:startColor="#FF5DA2FF" />
    <padding
        android:bottom="7dp"
        android:left="7dp"
        android:right="7dp"
        android:top="7dp" />
    <corners android:radius="8dp" />
</shape>

效果如图

在这里插入图片描述

  • shape:rectangle(矩形)、oval(椭圆)、line(横线)、ring(圆环),后两者需要<stroke>指定线宽和颜色

对于ring,有下列特殊属性

在这里插入图片描述

<corner>:指定四个角的角度,只用于rectangle

<gradient>:渐变效果

  • angle:渐变角度,值为45的倍数,默认0表示从左到右,90表示从上到下
  • centerX/Y:渐变中心点坐标
  • start/center/endColor:渐变起始/中间/结束颜色
  • type:渐变类型,liner(默认)、radial、sweep
  • gradientRadius:渐变半径,当type=radial时生效
  • userLevel:默认false,作为StateListDrawable时为true

<solid>:纯色填充

<stoke>:描边

  • width/color:宽度/颜色
  • dashWidth/dashGap:虚线 线段宽度/线段间隔,任一为0则虚线无效

<size>:设置固定宽高,可通过getIntrinsicWidth/Height()获取,并非最终宽高,作为View背景时还会被拉伸

layer-list

对应LayerDrawable,可实现类似PS中图层的概念

<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:drawable="@mipmap/ic_launcher" />
    <item
        android:bottom="50dp"
        android:drawable="@mipmap/ic_launcher"
        android:left="50dp"
        android:right="50dp"
        android:top="50dp" />
</layer-list>

上面两个item分别为图片进行叠加,效果如图

在这里插入图片描述

selector

对应StateListDrawable,实现不同事件设置反馈(默认状态应放在最后),如下实现修改为圆角矩形,点击后切换颜色

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:state_pressed="true">
        <shape android:shape="rectangle">
            <solid android:color="#33444444" />
            <corners android:radius="5dp" />
            <padding android:bottom="10dp" android:left="10dp" android:right="10dp" android:top="10dp" />
        </shape>
    </item>

    <item>
        <shape android:shape="rectangle">
            <solid android:color="#FFFFFF" />
            <corners android:radius="5dp" />
            <padding android:bottom="10dp" android:left="10dp" android:right="10dp" android:top="10dp" />
        </shape>
    </item>
</selector>
  • constantSize:固定大小是否随状态改变,true表示不变且大小为内部所有Drawable的最大值,默认为false
  • variablePadding:padding是否随状态改变,true表示改变,默认为false,不建议开启

level-list

对应LevelListDrawable,设置不同等级范围(默认0,最大10000)显示不同的Drawable

<?xml version="1.0" encoding="utf-8"?>
<level-list xmlns:android="http://schemas.android.com/apk/res/android">
    <item
        android:drawable="@drawable/ic_launcher_background"
        android:maxLevel="10"
        android:minLevel="6" />
    <item
        android:drawable="@drawable/ic_launcher"
        android:maxLevel="5"
        android:minLevel="0" />
</level-list>

当其作为background时,可通过setLevel()修改等级,当其作为ImageView的src时,可通过setImageLevel()修改等级

transition

对应TransitionDrawable,实现淡入淡出

<?xml version="1.0" encoding="utf-8"?>
<transition xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:drawable="@drawable/ic_launcher_background" />
    <item android:drawable="@drawable/ic_launcher" />
</transition>

如上设置为android:background

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <ImageView
        android:id="@+id/iv"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="@drawable/test" />

</LinearLayout>

通过startTransition()实现淡入淡出效果

public class MainActivity extends AppCompatActivity {

    private static final String TAG = "MainActivity";

    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ImageView iv = findViewById(R.id.iv);
        TransitionDrawable background = (TransitionDrawable) iv.getBackground();
        background.startTransition(3000);
    }
}

inset

对应InsetDrawable,将其他Drawable内嵌到自己当中(也可用layer替代),当希望背景比自己的实际区域小的时候就可以使用

<?xml version="1.0" encoding="utf-8"?>
<inset xmlns:android="http://schemas.android.com/apk/res/android"
    android:drawable="@drawable/ic_launcher_background"
    android:insetLeft="15dp"
    android:insetTop="15dp"
    android:insetRight="15dp"
    android:insetBottom="15dp"/>

在这里插入图片描述

scale

对应ScaleDrawable,根据自身的level和缩放比例进行缩放

  • sacleGravity:等同于android:gravity
  • scaleWidth/Height:宽/高缩放比例

必须设置level,默认为0不可见,不会调用draw()

@Override
public void draw(Canvas canvas) {
    final Drawable d = getDrawable();
    if (d != null && d.getLevel() != 0) {
        d.draw(canvas);
    }
}

如下iw一般为0,w -= (int) (w * (MAX_LEVEL - level) * mState.mScaleWidth / MAX_LEVEL)可知

  • level=10000时相当于未缩放
  • level的值越大,Drawable越大
  • scaleWidth/Height越大,Drawable越小
@Override
protected void onBoundsChange(Rect bounds) {
    final Drawable d = getDrawable();
    final Rect r = mTmpRect;
    final boolean min = mState.mUseIntrinsicSizeAsMin;
    final int level = getLevel();
    int w = bounds.width();
    if (mState.mScaleWidth > 0) {
        final int iw = min ? d.getIntrinsicWidth() : 0;
        w -= (int) ((w - iw) * (MAX_LEVEL - level) * mState.mScaleWidth / MAX_LEVEL);
    }
    int h = bounds.height();
    if (mState.mScaleHeight > 0) {
        final int ih = min ? d.getIntrinsicHeight() : 0;
        h -= (int) ((h - ih) * (MAX_LEVEL - level) * mState.mScaleHeight / MAX_LEVEL);
    }
    final int layoutDirection = getLayoutDirection();
    Gravity.apply(mState.mGravity, w, h, bounds, r, layoutDirection);
    if (w > 0 && h > 0) {
        d.setBounds(r.left, r.top, r.right, r.bottom);
    }
}

如下缩小图片为原来的30%

<?xml version="1.0" encoding="utf-8"?>
<scale xmlns:android="http://schemas.android.com/apk/res/android"
    android:drawable="@drawable/ic_launcher"
    android:scaleWidth="70%"
    android:scaleHeight="70%"
    android:scaleGravity="center" />
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <ImageView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="@drawable/ic_launcher" />

    <ImageView
        android:id="@+id/iv"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="@drawable/test" />

</LinearLayout>
public class MainActivity extends AppCompatActivity {

    private static final String TAG = "MainActivity";

    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ImageView iv = findViewById(R.id.iv);
        ScaleDrawable background = (ScaleDrawable) iv.getBackground();
        background.setLevel(1);
    }
}

在这里插入图片描述

clip

对应ClipDrawable,根据自身level裁剪一个Drawable,0表示完全裁剪,10000则完全不裁剪

  • clipOrientation:裁剪方向,水平和竖直
  • gravity有如下选项
    在这里插入图片描述

如下设置从上往下裁剪50%

<?xml version="1.0" encoding="utf-8"?>
<clip xmlns:android="http://schemas.android.com/apk/res/android"
    android:clipOrientation="vertical"
    android:drawable="@drawable/ic_launcher"
    android:gravity="bottom" />
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <ImageView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="@drawable/ic_launcher" />

    <ImageView
        android:id="@+id/iv"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="@drawable/test" />

</LinearLayout>
public class MainActivity extends AppCompatActivity {

    private static final String TAG = "MainActivity";

    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ImageView iv = findViewById(R.id.iv);
        ClipDrawable background = (ClipDrawable) iv.getBackground();
        background.setLevel(5000);
    }
}

在这里插入图片描述

自定义Drawable

Drawable一般作为ImageView中的图像显示,或者作为View的背景,但自定义Drawable无法在xml中使用,如下实现一个圆形Drawable,随着宽高变化而变化

public class CustomDrawable extends Drawable {

    private Paint mPaint;

    public CustomDrawable(int color) {
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setColor(color);
    }

    @Override
    public void draw(@NonNull Canvas canvas) {
        Rect bounds = getBounds();
        float exactCenterX = bounds.exactCenterX();
        float exactCenterY = bounds.exactCenterY();
        canvas.drawCircle(exactCenterX, exactCenterY, Math.min(exactCenterX, exactCenterY), mPaint);
    }

    @Override
    public void setAlpha(int alpha) {
        mPaint.setAlpha(alpha);
        invalidateSelf();
    }

    @Override
    public void setColorFilter(@Nullable ColorFilter colorFilter) {
        mPaint.setColorFilter(colorFilter);
        invalidateSelf();
    }

    @Override
    public int getOpacity() {
        return PixelFormat.TRANSLUCENT;
    }
}
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <View
        android:id="@+id/iv"
        android:layout_width="100dp"
        android:layout_height="100dp" />

</LinearLayout>
public class MainActivity extends AppCompatActivity {

    private static final String TAG = "MainActivity";

    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        View iv = findViewById(R.id.iv);
        iv.setBackground(new CustomDrawable(Color.RED));
    }
}

绘图技巧

Canvas

下面为常用的方法

  • save():将已绘制图像保存,在此之后的操作绘制在新图层
  • restore():合并图层,将save()之后绘制的图像和save()之前的合并
  • translate():坐标系平移
  • ratate():坐标系旋转

如下绘制一个时钟,通过平移、旋转坐标系简化实现

public class Clock extends View {


    private int mWidth;
    private int mHeight;

    public Clock(Context context) {
        super(context);
    }

    public Clock(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public Clock(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        mWidth = getMeasuredWidth();
        mHeight = getMeasuredHeight();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        // 绘制外圆,以(mWidth / 2, mHeight / 2)为圆形,以mWidth / 2为半径
        Paint paintCircle = new Paint();
        paintCircle.setStyle(Paint.Style.STROKE);
        paintCircle.setAntiAlias(true);
        paintCircle.setStrokeWidth(5);
        canvas.drawCircle(mWidth / 2, mHeight / 2, mWidth / 2, paintCircle);

        // 绘制刻度
        Paint paintDegree = new Paint();
        for (int i = 0; i < 24; i++) {
            if (i == 0 || i == 6 || i == 12 || i == 18) {
                paintDegree.setStrokeWidth(5);
                paintDegree.setTextSize(30);
                canvas.drawLine(mWidth / 2, mHeight / 2 - mWidth / 2,
                        mWidth / 2, mHeight / 2 - mWidth / 2 + 60,
                        paintDegree);
                String degree = String.valueOf(i);
                canvas.drawText(degree,
                        mWidth / 2 - paintDegree.measureText(degree) / 2,
                        mHeight / 2 - mWidth / 2 + 90,
                        paintDegree);
            } else {
                paintDegree.setStrokeWidth(3);
                paintDegree.setTextSize(15);
                canvas.drawLine(mWidth / 2, mHeight / 2 - mWidth / 2,
                        mWidth / 2, mHeight / 2 - mWidth / 2 + 30,
                        paintDegree);
                String degree = String.valueOf(i);
                canvas.drawText(degree,
                        mWidth / 2 - paintDegree.measureText(degree) / 2,
                        mHeight / 2 - mWidth / 2 + 60,
                        paintDegree);
            }
            // 将画布以圆心旋转15°,简化坐标运算
            canvas.rotate(15, mWidth / 2, mHeight / 2);
        }

        // 绘制指针
        Paint paintHour = new Paint();
        paintHour.setStrokeWidth(20);
        Paint paintMinute = new Paint();
        paintMinute.setStrokeWidth(10);
        canvas.save();

        canvas.translate(mWidth / 2, mHeight / 2); // 将坐标系平移到圆点,再画指针
        canvas.drawLine(0, 0, 100, 100, paintHour);
        canvas.drawLine(0, 0, 100, 200, paintMinute);
        canvas.restore();
    }
}

Layer

图层基于栈结构,入栈后操作都发生在该图层上,出栈后则把图像绘制到上层Canvas

  • 入栈:saveLayer()、saveLayerAlpha()
  • 出栈:restore()、restoreToCount()
public class MyView extends View {

    private static final String TAG = MyView.class.getSimpleName();
    private Paint mPaint;

    public MyView(Context context) {
        this(context, null);
    }

    public MyView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public MyView(Context context, AttributeSet attrs, int defStyleAttr) {
        this(context, attrs, defStyleAttr, 0);
    }

    public MyView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        mPaint = new Paint();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        canvas.drawColor(Color.WHITE);
        mPaint.setColor(Color.BLUE);
        canvas.drawCircle(150, 150, 100, mPaint);

        canvas.saveLayerAlpha(0, 0, 400, 400, 127);
        mPaint.setColor(Color.RED);
        canvas.drawCircle(200, 200, 100, mPaint);
        canvas.restore();
    }
}

如下图,透明度为127的红色圆叠加在蓝色圆上方

在这里插入图片描述

PorterDuffXfermode

PorterDuffXfermod设置的是两个图层交集区域的显示方式,dst是先画的图形,而src是后画的图形,共有16种模式,如图

在这里插入图片描述

如下实现圆角图形,mOut为先画的图像,mBitmap为后画的图像,用SrcIn取交集

public class MyView extends View {

    private static final String TAG = MyView.class.getSimpleName();
    private Bitmap mBitmap;
    private Bitmap mOut;
    private Paint mPaint;

    public MyView(Context context) {
        this(context, null);
    }

    public MyView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public MyView(Context context, AttributeSet attrs, int defStyleAttr) {
        this(context, attrs, defStyleAttr, 0);
    }

    public MyView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        mBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.ic_launcher);
        mOut = Bitmap.createBitmap(mBitmap.getWidth(), mBitmap.getHeight(), Bitmap.Config.ARGB_8888);
        Canvas canvas = new Canvas(mOut);
        mPaint = new Paint();
        mPaint.setAntiAlias(true);
        canvas.drawRoundRect(0, 0, mBitmap.getWidth(), mBitmap.getHeight(), 80, 80, mPaint);
        mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
        canvas.drawBitmap(mBitmap, 0, 0, mPaint);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawBitmap(mOut, 0, 0, null);
    }
}

效果如图

在这里插入图片描述

如下代码设置透明的画笔,实时获取坐标调用drawPath()涂抹上面的图层,实现刮刮乐

public class XfermodeView extends View {

    private Bitmap mBgBitmap, mFgBitmap;
    private Paint mPaint;
    private Canvas mCanvas;
    private Path mPath;

    public XfermodeView(Context context) {
        this(context, null);
    }

    public XfermodeView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public XfermodeView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init() {
        mPaint = new Paint();
        mPaint.setAlpha(0);
        mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_IN));
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setStrokeJoin(Paint.Join.ROUND);
        mPaint.setStrokeWidth(50);
        mPaint.setStrokeCap(Paint.Cap.ROUND);
        mPath = new Path();
        mBgBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.b);
        mFgBitmap = Bitmap.createBitmap(mBgBitmap.getWidth(), mBgBitmap.getHeight(), Bitmap.Config.ARGB_8888);
        mCanvas = new Canvas(mFgBitmap);
        mCanvas.drawColor(Color.GRAY);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mPath.reset();
                mPath.moveTo(event.getX(), event.getY());
                break;
            case MotionEvent.ACTION_MOVE:
                mPath.lineTo(event.getX(), event.getY());
                break;
        }
        mCanvas.drawPath(mPath, mPaint);
        invalidate();
        return true;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        canvas.drawBitmap(mBgBitmap, 0, 0, null);
        canvas.drawBitmap(mFgBitmap, 0, 0, null);
    }
}

在这里插入图片描述

Shader

用于实现渐变、渲染效果,包括

  • BitmapShader——位图Shader
  • LinnerGradient——线性Shader
  • RadialGradient——光束Shader
  • SweepGradient——梯度Shader
  • ComposeShader——混合Shader

上述渐变都有三种模式选择:

  • CLAMP:拉伸,拉伸的是图片最后一个像素,不断重复
  • REPEAT:重复,横向、纵向不断重复
  • MIRROR:镜像,横向、纵向不断翻转重复

如下,将图片填充到BitmapShader,创建一支具有图像填充功能的Paint

public class MyView extends View {

    private static final String TAG = MyView.class.getSimpleName();
    private Bitmap mBitmap;
    private Paint mPaint;
    private BitmapShader mBitmapShader;

    public MyView(Context context) {
        this(context, null);
    }

    public MyView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public MyView(Context context, AttributeSet attrs, int defStyleAttr) {
        this(context, attrs, defStyleAttr, 0);
    }

    public MyView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        mBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.ic_launcher);
        mBitmapShader = new BitmapShader(mBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
        mPaint = new Paint();
        mPaint.setShader(mBitmapShader);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawCircle(mBitmap.getWidth() / 2.0f, mBitmap.getHeight() / 2.0f, 50, mPaint);
    }
}

利用Paint在图片中心绘制圆形,效果如下

在这里插入图片描述
如下改为REPEAT,并扩大半径,可看到图片不断重复绘制

public class MyView extends View {

    private static final String TAG = MyView.class.getSimpleName();
    private Bitmap mBitmap;
    private Paint mPaint;
    private BitmapShader mBitmapShader;

    public MyView(Context context) {
        this(context, null);
    }

    public MyView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public MyView(Context context, AttributeSet attrs, int defStyleAttr) {
        this(context, attrs, defStyleAttr, 0);
    }

    public MyView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        mBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.ic_launcher);
        mBitmapShader = new BitmapShader(mBitmap, Shader.TileMode.REPEAT, Shader.TileMode.REPEAT);
        mPaint = new Paint();
        mPaint.setShader(mBitmapShader);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawCircle(mBitmap.getWidth() / 2.0f, mBitmap.getHeight() / 2.0f, 500, mPaint);
    }
}

在这里插入图片描述

如下利用PorterDuffXfermode和LinnerGradient实现倒影效果的图片

public class ReflectView extends View {
    private Bitmap mSrcBitmap, mRefBitmap;
    private Paint mPaint;
    private PorterDuffXfermode mXfermode;

    public ReflectView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    private void init() {
        mSrcBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.b);

        Matrix matrix = new Matrix();
        matrix.setScale(1F, -1F);//垂直翻转

        mRefBitmap = Bitmap.createBitmap(mSrcBitmap, 0, 0,
                mSrcBitmap.getWidth(), mSrcBitmap.getHeight(), matrix, true);

        mPaint = new Paint();
        mPaint.setShader(new LinearGradient(0, mSrcBitmap.getHeight(),
                0, mSrcBitmap.getHeight() * 2,
                0xDD000000, 0x10000000, Shader.TileMode.REPEAT));

        mXfermode = new PorterDuffXfermode(PorterDuff.Mode.DST_IN);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawColor(Color.BLACK);
        //原图
        canvas.drawBitmap(mSrcBitmap, 0, 0, null);
        //倒影图
        canvas.drawBitmap(mRefBitmap, 0, mSrcBitmap.getHeight(), null);

        mPaint.setXfermode(mXfermode);
        //绘制渐变效果层
        canvas.drawRect(0, mRefBitmap.getHeight(), mRefBitmap.getWidth(), mSrcBitmap.getHeight() * 2, mPaint);
        mPaint.setXfermode(null);
    }
}

先将图片翻转,再添加黑色透明度递减的渐变,DST_IN是为了避免上面的图像被渐变效果遮挡

在这里插入图片描述

PathEffect

PathEffect指用各种笔触效果来绘制一个路径
在这里插入图片描述

如上,分别为

  • 无效果
  • CornerPathEffect:线段拐角圆滑
  • DiscretePathEffect:线段出现杂点
  • DashPathEffect:绘制虚线,用一个数组来设置各个点之间的间隔,还可设置偏移量实现动态路径效果
  • PathPathEffect:类似DashPathEffect,但还可设置点的图形,如上面的圆点虚线
  • ConposePathEffect:组合效果
public class MyView extends View {

    private static final String TAG = MyView.class.getSimpleName();
    private Path mPath;
    private PathEffect[] mPathEffect;
    private Paint mPaint;

    public MyView(Context context) {
        this(context, null);
    }

    public MyView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public MyView(Context context, AttributeSet attrs, int defStyleAttr) {
        this(context, attrs, defStyleAttr, 0);
    }

    public MyView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        mPaint = new Paint();
        mPaint.setStyle(Paint.Style.STROKE);
        mPath = new Path();
        mPath.moveTo(0, 0);
        for (int i = 0; i <= 30; i++) {
            mPath.lineTo(i * 35, (float) (Math.random() * 100));
        }
        mPathEffect = new PathEffect[6];
        mPathEffect[0] = null;
        mPathEffect[1] = new CornerPathEffect(30);
        mPathEffect[2] = new DiscretePathEffect(3.0F, 5.0F);
        mPathEffect[3] = new DashPathEffect(new float[]{20, 10, 5, 10}, 0);
        Path path = new Path();
        path.addRect(0, 0, 8, 8, Path.Direction.CCW);
        mPathEffect[4] = new PathDashPathEffect(path, 12, 0, PathDashPathEffect.Style.ROTATE);
        mPathEffect[5] = new ComposePathEffect(mPathEffect[3], mPathEffect[1]);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        for (PathEffect pathEffect : mPathEffect) {
            mPaint.setPathEffect(pathEffect);
            canvas.drawPath(mPath, mPaint);
            canvas.translate(0, 200);
        }
    }
}

SurfaceView

Android通过VSYNC信号刷新View进行屏幕的重绘,时间间隔为16ms,若在需要频繁刷新的界面上执行太多的操作逻辑,则容易阻塞主线程,造成画面卡顿

当需要频繁刷新或刷新时数据处理量比较大时,可使用SurfaceView

  • View用于主动刷新,SurfaceView用于被动刷新
  • View在主线程刷新,SurfaceView在子线程刷新
  • View绘图未使用双缓冲机制,SurfaceView则使用双缓冲机制

SurfaceView的模板代码如下,主要原理是初始化时自行创建子线程并调用draw()方法绘制

public class SurfaceViewTemplate extends SurfaceView implements SurfaceHolder.Callback, Runnable {

    //内部Holder,用于获取Canvas和提交绘制
    private SurfaceHolder mHolder;
    //用于控制线程的标志位
    private boolean mIsDrawing;
    //用于绘图的Canvas
    private Canvas mCanvas;

    public SurfaceViewTemplate(Context context) {
        this(context, null);
    }

    public SurfaceViewTemplate(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public SurfaceViewTemplate(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init() {
        mHolder = getHolder();
        mHolder.addCallback(this);  //将自身作为回调

        setFocusableInTouchMode(true);
        setFocusable(true);

        this.setKeepScreenOn(true);
    }

    //创建时开启线程,调用draw()
    @Override
    public void surfaceCreated(SurfaceHolder holder) {
        mIsDrawing = true;

        new Thread(this).start();
    }

    //改变时回调
    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {

    }

    //结束时回调
    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
        mIsDrawing = false;
    }

    @Override
    public void run() {
        while (mIsDrawing) {
            draw();
        }
    }

    private void draw() {
        try {
            mCanvas = mHolder.lockCanvas();
            //获取Canvas开始绘制,获取的并非新的Canvas,而是上次的
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (mHolder != null) {
                //绘制完后需提交
                mHolder.unlockCanvasAndPost(mCanvas);
            }
        }
    }
}

如下代码利用SurfaceView实现正弦函数的动态绘制

public class SurfaceViewTemplate extends SurfaceView implements SurfaceHolder.Callback, Runnable {

    private SurfaceHolder mHolder;
    private boolean mIsDrawing;
    private Canvas mCanvas;
    private int x;
    private int y;
    private Path mPath;
    private Paint mPaint;

    public SurfaceViewTemplate(Context context) {
        this(context, null);
    }

    public SurfaceViewTemplate(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public SurfaceViewTemplate(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init() {
        mPath = new Path();
        mPaint = new Paint();
        mPaint.setStyle(Paint.Style.STROKE);

        mHolder = getHolder();
        mHolder.addCallback(this);

        setFocusableInTouchMode(true);
        setFocusable(true);

        this.setKeepScreenOn(true);
    }

    @Override
    public void surfaceCreated(SurfaceHolder holder) {
        mIsDrawing = true;

        new Thread(this).start();
    }

    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {

    }

    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
        mIsDrawing = false;
    }

    @Override
    public void run() {
        while (mIsDrawing) {
            draw();
            x += 1;
            y = (int) (100 * Math.sin(x * 2 * Math.PI / 180) + 400);
            mPath.lineTo(x, y);
        }
    }

    private void draw() {
        try {
            mCanvas = mHolder.lockCanvas();
            mCanvas.drawColor(Color.WHITE); //设置背景
            mCanvas.drawPath(mPath, mPaint);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (mHolder != null) {
                mHolder.unlockCanvasAndPost(mCanvas);
            }
        }
    }
}

效果如图

在这里插入图片描述

如下代码利用SurfaceView实现绘图板,根据手指移动绘制路径,在draw()中调用sleep()避免过度频繁绘制

public class SurfaceViewTemplate extends SurfaceView implements SurfaceHolder.Callback, Runnable {

    private SurfaceHolder mHolder;
    private boolean mIsDrawing;
    private Canvas mCanvas;
    private Path mPath;
    private Paint mPaint;

    public SurfaceViewTemplate(Context context) {
        this(context, null);
    }

    public SurfaceViewTemplate(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public SurfaceViewTemplate(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init() {
        mPath = new Path();
        mPaint = new Paint();
        mPaint.setStyle(Paint.Style.STROKE);

        mHolder = getHolder();
        mHolder.addCallback(this);

        setFocusableInTouchMode(true);
        setFocusable(true);

        this.setKeepScreenOn(true);
    }

    @Override
    public void surfaceCreated(SurfaceHolder holder) {
        mIsDrawing = true;

        new Thread(this).start();
    }

    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {

    }

    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
        mIsDrawing = false;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int x = (int) event.getX();
        int y = (int) event.getY();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mPath.moveTo(x, y);
                break;
            case MotionEvent.ACTION_MOVE:
                mPath.lineTo(x, y);
                break;
            case MotionEvent.ACTION_UP:
                break;
        }
        return true;
    }

    @Override
    public void run() {
        long start = System.currentTimeMillis();
        while (mIsDrawing) {
            draw();
        }
        long end = System.currentTimeMillis();
        if (end - start < 100) {
            SystemClock.sleep(100 - (end - start));
        }
    }

    private void draw() {
        try {
            mCanvas = mHolder.lockCanvas();
            mCanvas.drawColor(Color.WHITE);
            mCanvas.drawPath(mPath, mPaint);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (mHolder != null) {
                mHolder.unlockCanvasAndPost(mCanvas);
            }
        }
    }
}

效果如图

在这里插入图片描述

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值