高级UI-画板Canvas

Canvas可以用来绘制直线、点、几何图形、曲线、Bitmap、圆弧等等,做出很多很棒的效果,例如QQ的消息气泡就是使用Canvas画的

Canvas中常用的方法

  • 初始化参数
Paint paint = new Paint();
paint.setColor(Color.RED);
paint.setStyle(Paint.Style.FILL);
paint.setStrokeWidth(8);
  • 绘制直线
canvas.drawLine(0, 0, 100, 100, paint);
  • 绘制一组直线
float[] lines = {0, 0, 100, 100, 200, 200, 300, 300};
canvas.drawLines(lines,paint);
  • 绘制点
canvas.drawPoint(100, 100, paint);
  • 绘制矩形
Rect rect = new Rect(0, 0, 200, 100);
canvas.drawRect(rect, paint);
//canvas.drawRect(0, 0, 200, 100, paint);
  • 绘制圆角矩形
RectF rectF = new RectF(100, 100, 300, 200);
canvas.drawRoundRect(rectF, 20, 20, paint);
  • 绘制圆形
canvas.drawCircle(300, 300, 200, paint);
canvas.drawOval(100, 100, 300, 200, paint);
  • 绘制弧度
RectF rectF = new RectF(100, 100, 300, 200);
canvas.drawArc(rectF, 0, 90, true, paint);

使用Path参与绘制

  • 绘制直线
//使用Path
Path path = new Path();
//落笔位置
path.moveTo(100, 100);
//移动
path.lineTo(200, 100);
path.lineTo(200, 200);
//闭合线
path.close();
//按路径绘制
canvas.drawPath(path, paint);
  • 绘制其他线条,使用path.addXxx()
float[] radii = {10, 10, 20, 30, 40, 40, 60, 50};
RectF rectF = new RectF(100, 100, 600, 500);
path.addRoundRect(rectF, radii, Path.Direction.CCW);
canvas.drawPath(path, paint);

使用Region区域绘制

//创建一块矩形区域
Region region = new Region(100, 100, 500, 400);
RegionIterator iterator = new RegionIterator(region);
Rect rect = new Rect();
while (iterator.next(rect)) {
    canvas.drawRect(rect, paint);
}

以上只是画出一个矩形,另外并没有什么现象,这是因为只有一个Region
两个Region实现取交集,使用并且不断分割交集部分

Path path = new Path();
RectF rectF = new RectF(100, 100, 600, 800);
path.addOval(rectF, Path.Direction.CCW);
Region region1 = new Region();
Region region2 = new Region(100, 100, 500, 400);
region1.setPath(path, region2);
RegionIterator iterator = new RegionIterator(region1);
Rect rect = new Rect();
while (iterator.next(rect)) {
	canvas.drawRect(rect, paint);
}

以上执行结果如下
画板Canvas-Region效果
Region的其他操作
并集:region.union(r);
交集:region.op(r, Op.INTERSECT);

Canvas的细节问题

当canvas执行drawXXX的时候就会新建一个新的画布图层
虽然新建一个画布图层,但是还是会沿用之前设置的平移变换,不可逆的(save和restore来解决)
之所以这样设计,是考虑到了绘制复杂图形的时候,可能会变换画布位置,那么就会造成之前绘制的图像发生错位,导致前功尽弃

Canvas变换

平移(Translate)

Rect rect = new Rect(100, 100, 800, 1000);
paint.setColor(Color.RED);
canvas.drawRect(rect, paint);
canvas.translate(100, 100);
paint.setColor(Color.GREEN);
canvas.drawRect(rect, paint);

画板Canvas-平移

缩放(Scale)

Rect rect = new Rect(100, 100, 600, 800);
paint.setColor(Color.RED);
canvas.drawRect(rect, paint);
canvas.scale(1.2F, 1.5F);
paint.setColor(Color.GREEN);
canvas.drawRect(rect, paint);

画板Canvas-缩放

旋转(Rotate)

Rect rect = new Rect(400, 100, 700, 800);
paint.setColor(Color.RED);
canvas.drawRect(rect, paint);
canvas.rotate(25);//默认围绕原点
//canvas.rotate(25, 400, 100);//指定旋转点
paint.setColor(Color.GREEN);
canvas.drawRect(rect, paint);

画板Canvas-旋转

斜拉画布(Skew)

Rect rect = new Rect(100, 100, 400, 800);
paint.setColor(Color.RED);
canvas.drawRect(rect, paint);
canvas.skew(0.5F, 0);
paint.setColor(Color.GREEN);
canvas.drawRect(rect, paint);

画板Canvas-斜拉

裁剪画布(clip)

RectF rectF = new RectF(100, 100, 500, 800);
paint.setColor(Color.RED);
canvas.drawRect(rectF, paint);
paint.setColor(Color.GREEN);
canvas.clipRect(new Rect(200, 200, 400, 400));
canvas.drawColor(Color.BLUE);

画板Canvas-裁剪

变换操作的影响

当对画板进行操作以后,会对后续的画布操作造成影响,那么要实现不对后续操作就需要在操作前保存画布,需要时候恢复画布

canvas.save(); //保存当前画布
···//画布操作
canvas.restore();//恢复保存的画布

canvas实际上是保存到画布栈里面去了,每一次保存,就使得一个当前画布入栈,每一次恢复,就有一个canvas出栈
因此,一般来说保存和恢复是成对出现的

自定义Drawable动画

Drawable就是一个可画的对象,其可能是一张位图(BitmapDrawable),也可能是一个图形(ShapeDrawable),还有可能是一个图层(LayerDrawable),我们根据画图的需求,创建相应的可画对象,就可以将这个可画对象当作一块“画布(Canvas)”,在其上面操作可画对象,并最终将这种可画对象显示在画布上,有点类似于"内存画布"
自定义Drawable

public class RevealDrawableView extends Drawable {

    private int orientation;
    private Drawable selected;
    private Drawable unselected;
    private int widthLeft;
    private int heightLeft;
    private int widthRight;
    private int heightRight;
    public static final int HORIZONTAL = 1;
    public static final int VERTICAL = 2;

    public RevealDrawableView(Drawable unselected, Drawable selected, int orientation) {
        this.unselected = unselected;
        this.selected = selected;
        this.orientation = orientation;
    }

    @Override
    public void draw(@NonNull Canvas canvas) {

        //得到当前level,转化成百分比
        int level = getLevel();
        if (level == 10000 || level == 0) { //滑出画入状态
            unselected.draw(canvas);
        } else if (level == 5000) { //在正中间状态
            selected.draw(canvas);
        } else {
            Rect bounds = getBounds();//得到当前Drawable自身的矩形区域
            float ratio = (level / 5000F) - 1F;//得到比例-1~+1
            int width = bounds.width();
            int height = bounds.height();
            widthLeft = widthRight = width;
            heightLeft = heightRight = height;
            //得到左右宽高
            if (orientation == HORIZONTAL) {
                widthLeft = (int) (width * Math.abs(ratio));
                widthRight = width - widthLeft;
            }
            if (orientation == VERTICAL) {
                heightLeft = (int) (height * Math.abs(ratio));
                heightRight = height - heightLeft;
            }
            int gravity = ratio < 0 ? Gravity.LEFT : Gravity.RIGHT;
            //得到当前左边区域
            Rect rectLeft = new Rect();
            //抠图位置,宽度,高度,目标,输出位置
            Gravity.apply(gravity, widthLeft, heightLeft, bounds, rectLeft);
            canvas.save();//保存画布
            canvas.clipRect(rectLeft);//剪切画布
            unselected.draw(canvas);//画未选中图片
            canvas.restore();//恢复画布

            //得到右边矩形区域
            gravity = ratio < 0 ? Gravity.RIGHT : Gravity.LEFT;
            Rect rectRight = new Rect();
            //抠图位置,宽度,高度,目标,输出位置
            Gravity.apply(gravity, widthRight, heightRight, bounds, rectRight);
            canvas.save();
            canvas.clipRect(rectRight);
            selected.draw(canvas);//画选中图片
            canvas.restore();
        }
    }

    @Override
    protected void onBoundsChange(Rect bounds) {
        //设置矩形
        unselected.setBounds(bounds);
        selected.setBounds(bounds);
    }

    @Override
    public int getIntrinsicWidth() {
        //得到Drawable的实际宽度
        return Math.max(unselected.getIntrinsicWidth(), selected.getIntrinsicWidth());
    }

    @Override
    public int getIntrinsicHeight() {
        //得到Drawable的实际高度
        return Math.max(unselected.getIntrinsicHeight(), selected.getIntrinsicHeight());
    }

    @Override
    protected boolean onLevelChange(int level) {
        //每次level改变时刷新
        invalidateSelf();
        return true;
    }

    @Override
    public void setAlpha(int alpha) {

    }

    @Override
    public void setColorFilter(@Nullable ColorFilter colorFilter) {

    }

    @Override
    public int getOpacity() {
        return PixelFormat.UNKNOWN;
    }
}

自定义控件

public class GallaryHScrollView extends HorizontalScrollView implements OnTouchListener {

    private LinearLayout container;
    private int centerX;
    private int width;

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

    public GallaryHScrollView(Context context) {
        super(context);
        init();
    }

    private void init() {
        //ScrollView水平滑动,存在大量ImageView
        LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
                LinearLayout.LayoutParams.WRAP_CONTENT,
                LinearLayout.LayoutParams.WRAP_CONTENT);
        container = new LinearLayout(getContext());
        container.setLayoutParams(params);
        setOnTouchListener(this);
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        super.onLayout(changed, l, t, r, b);
        //得到某一张图片的宽度
        View view = container.getChildAt(0);
        width = view.getWidth();
        //得到中间x坐标
        centerX = getWidth() / 2;
        //中心坐标改为中心图片的左边界
        centerX = centerX - width / 2;
        //给LinearLayout和hzv之间设置边框距离
        container.setPadding(centerX, 0, centerX, 0);
    }

    @Override
    public boolean onTouch(View v, MotionEvent event) {
        if (event.getAction() == MotionEvent.ACTION_MOVE) {
            //渐变图片
            reveal();
        }
        return false;
    }

    private void reveal() {
        // 渐变效果
        //得到滑出去的距离
        int scrollX = getScrollX();
        //找到两张渐变的图片的下标--左,右
        int index_left = scrollX / width;
        int index_right = index_left + 1;
        //设置图片的level
        for (int i = 0; i < container.getChildCount(); i++) {
            if (i == index_left || i == index_right) {
                //变化
                float ratio = 5000f / width;//比例
                ImageView iv_left = (ImageView) container.getChildAt(index_left);
                //scrollX%icon_width:代表滑出去的距离
                iv_left.setImageLevel((int) (5000 - scrollX % width * ratio));
                //右边
                if (index_right < container.getChildCount()) {
                    ImageView iv_right = (ImageView) container.getChildAt(index_right);
                    //scrollX%icon_width:代表滑出去的距离
                    //滑出去了icon_width/2  icon_width/2%icon_width
                    iv_right.setImageLevel((int) (10000 - scrollX % width * ratio));
                }
            } else {
                //灰色
                ImageView iv = (ImageView) container.getChildAt(i);
                iv.setImageLevel(0);
            }
        }
    }

    //添加图片的方法
    public void addImageViews(Drawable[] revealDrawables) {
        for (int i = 0; i < revealDrawables.length; i++) {
            ImageView img = new ImageView(getContext());
            img.setImageDrawable(revealDrawables[i]);
            container.addView(img);
            if (i == 0) {
                img.setImageLevel(5000);
            }
        }
        addView(container);
    }
}

测试工具类

public class SourceUtils {
    private static int[] mImgIds = new int[]{
            R.drawable.avft, R.drawable.box_stack, R.drawable.bubble_frame,
            R.drawable.bubbles, R.drawable.bullseye, R.drawable.circle_filled,
            R.drawable.circle_outline, R.drawable.avft, R.drawable.box_stack,
            R.drawable.bubble_frame, R.drawable.bubbles, R.drawable.bullseye,
            R.drawable.circle_filled, R.drawable.circle_outline
    };
    private static int[] mImgIds_active = new int[]{
            R.drawable.avft_active, R.drawable.box_stack_active, R.drawable.bubble_frame_active,
            R.drawable.bubbles_active, R.drawable.bullseye_active, R.drawable.circle_filled_active,
            R.drawable.circle_outline_active, R.drawable.avft_active, R.drawable.box_stack_active,
            R.drawable.bubble_frame_active, R.drawable.bubbles_active, R.drawable.bullseye_active,
            R.drawable.circle_filled_active, R.drawable.circle_outline_active
    };

    public static int[] getImgIds() {
        return mImgIds;
    }

    public static int[] getImgIdsActive() {
        return mImgIds_active;
    }
}

布局

<?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">

    <com.cj5785.testcanvas.GallaryHScrollView
        android:id="@+id/ghs"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="center_vertical"
        android:background="@android:color/darker_gray"
        android:scrollbars="none" />

</LinearLayout>

测试

public class RevealDrawableActivity extends AppCompatActivity {

    private Drawable[] revealDrawables;
    private GallaryHScrollView gallaryHScrollView;
    protected int level = 10000;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_reveal_drawable);
        revealDrawables = new Drawable[SourceUtils.getImgIds().length];
        for (int i = 0; i < SourceUtils.getImgIds().length; i++) {
            RevealDrawableView rd = new RevealDrawableView(
                    getResources().getDrawable(SourceUtils.getImgIds()[i]),
                    getResources().getDrawable(SourceUtils.getImgIdsActive()[i]),
                    RevealDrawableView.HORIZONTAL);
            revealDrawables[i] = rd;
        }
        gallaryHScrollView = (GallaryHScrollView) findViewById(R.id.ghs);
        gallaryHScrollView.addImageViews(revealDrawables);
    }
}

效果
画板Canvas-自定义Drawable

自定义控件Search动画

自定义View控件

public class SearchView extends View {
    private Paint paint;
    private BaseController controller;

    public SearchView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    private void init() {
        paint = new Paint();
        paint.setStrokeWidth(8);
    }

    public void setController(BaseController controller) {
        this.controller = controller;
        controller.setSearchView(this);
        invalidate();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        controller.draw(canvas, paint);
    }

    public void startAnimation() {
        if (controller != null) {
            controller.startAnim();
        }
    }

    public void resetAnimation() {
        if (controller != null) {
            controller.resetAnim();
        }
    }
}

通过控制器来控制绘画

public abstract class BaseController {

    public static final int STATE_ANIM_RESET = 0;
    public static final int STATE_ANIM_START = 1;
    public int state = STATE_ANIM_RESET;

    public float progress = -1;
    private SearchView searchView;

    public abstract void draw(Canvas canvas, Paint paint);

    public void startAnim() {

    }

    public void resetAnim() {

    }

    public int getWidth() {
        return searchView.getWidth();
    }

    public int getHeight() {
        return searchView.getHeight();
    }

    public ValueAnimator startValueAnimation() {
        final ValueAnimator animator = ValueAnimator.ofFloat(0, 1);
        animator.setDuration(5000);
        animator.setInterpolator(new AnticipateInterpolator());
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                progress = (float) animator.getAnimatedValue();
                searchView.invalidate();
            }
        });
        animator.start();
        progress = 0;
        return animator;
    }

    public void setSearchView(SearchView searchView) {
        this.searchView = searchView;
    }
}

实现控制器方法

public class MyController extends BaseController {

    private int color = Color.GREEN;
    private int cx, cy, cr;
    private RectF rectF;
    private int j = 15;

    public MyController() {
        rectF = new RectF();
    }

    @Override
    public void draw(Canvas canvas, Paint paint) {
        canvas.drawColor(color);
        switch (state) {
            case STATE_ANIM_START:
                drawStartAnimView(canvas, paint);
                break;
            case STATE_ANIM_RESET:
                drawResetAnimView(canvas, paint);
                break;
        }
    }

    private void drawStartAnimView(Canvas canvas, Paint paint) {
        canvas.save();
        if (progress <= 0.5f) {
            //绘制圆和把手
            canvas.drawArc(rectF, 45, 360 * (progress * 2 - 1),
                    false, paint);
            canvas.drawLine(rectF.right - j, rectF.bottom - j,
                    rectF.right + cr - j, rectF.bottom + cr - j, paint);
        } else {
            //绘制把手
            canvas.drawLine(
                    rectF.right - j + cr * (progress * 2 - 1),
                    rectF.bottom - j + cr * (progress * 2 - 1),
                    rectF.right - j + cr, rectF.bottom + cr - j, paint);
        }
        //绘制下面的横线
        canvas.drawLine(
                (rectF.right - j + cr) * (1 - progress * 0.8f), rectF.bottom + cr - j,
                rectF.right - j + cr, rectF.bottom + cr - j, paint);
        canvas.restore();
        rectF.left = cx - cr + progress * 250;
        rectF.right = cx + cr + progress * 250;
        rectF.top = cy - cr;
        rectF.bottom = cy + cr;
    }

    private void drawResetAnimView(Canvas canvas, Paint paint) {
        cr = getWidth() / 20;
        cx = getWidth() / 2;
        cy = getHeight() / 2;
        rectF.left = cx - cr;
        rectF.right = cx + cr;
        rectF.top = cy - cr;
        rectF.bottom = cy + cr;
        canvas.save();
        paint.reset();
        paint.setAntiAlias(true);
        paint.setColor(Color.WHITE);
        paint.setStrokeWidth(5);
        paint.setStyle(Paint.Style.STROKE);
        canvas.rotate(45, cx, cy);
        canvas.drawLine(cx + cr, cy, cx + cr * 2, cy, paint);
        canvas.drawArc(rectF, 0, 360, false, paint);
        canvas.restore();
    }

    @Override
    public void startAnim() {
        super.startAnim();
        state = STATE_ANIM_START;
        startValueAnimation();
    }

    @Override
    public void resetAnim() {
        super.resetAnim();
        state = STATE_ANIM_RESET;
        startValueAnimation();
    }
}

布局

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

    <com.cj5785.testcanvas.search.SearchView
        android:id="@+id/search_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:layout_alignParentLeft="true"
        android:text="start"
        android:onClick="start"/>

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:layout_alignParentRight="true"
        android:text="reset"
        android:onClick="reset"/>

</RelativeLayout>

测试

public class SearchActivity extends AppCompatActivity {

    private SearchView serachView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_search);
        serachView = findViewById(R.id.search_view);
        serachView.setController(new MyController());
    }

    public void start(View view) {
        serachView.startAnimation();
    }

    public void reset(View view) {
        serachView.resetAnimation();
    }
}

测试结果
画板Canvas-自定义search

posted @ 2019-04-06 23:29  cj5785  阅读(556)  评论(0编辑  收藏  举报