Android 自定义 View 实战之 PuzzleView

本篇文章为利用Matrix自定义View的第二篇,第一篇见Android自定义View实战之StickerViewjava

在阅读本篇文章以前,但愿你们有基本的自定义View知识和Matrix的知识,固然最好阅读了前一篇,由于不少东西是相通的,本文的重点在于前期的思考,至于具体实现细节能够不看,选择看源码git

起步

在图片的处理软件中,拼图是很常见的一种处理方法,我最喜欢Layout for Instagram的拼图效果,简单却又足够强大,拼图方式多种多样能够对图片进行水平垂直翻转,移位,移动,缩放,改变大小之类的操做,看到这样的操做。本文制做的View正是为了实现这个功能。先看最终咱们实现的效果。github

多种布局canvas

具体布局编辑ide

项目地址:github.com/wuapnjie/Pu…布局

肯定思路

在前面介绍中,咱们知道这一次咱们仍是对图片的一系列变换操做,那么此次咱们的实现思路也是在onTouchEvent()中根据手势控制对应的Matrix来对所画在View上的图片进行操做。post

再仔细看咱们的效果,在一个View中咱们可能要画上许多张图片,可是位置都不一样,且互相不会覆盖,那么能够看出咱们对View进行了分割,分红不一样的矩形,了解canvas的同窗知道,canvas能够先进行一系列变换后再进行绘制,绘制完成后恢复,此次利用的就是canvasclipRect()方法将canvas分红不一样的矩形区域进行绘制,先来看看大体效果可不能够达到咱们的预期。学习

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

    canvas.save();
    canvas.clipRect(0, 0, getWidth() / 2, getHeight());
    canvas.drawBitmap(mBitmapOne, 0, 0, mBitmapPaint);
    canvas.restore();

    canvas.save();
    canvas.clipRect(getWidth() / 2, 0, getWidth(), getHeight());
    canvas.drawBitmap(mBitmapTwo, 0, 0, mBitmapPaint);
    canvas.restore();
}复制代码

能够看到,这样是能够达到咱们想要的图片排列方式的,只须要对图片进行矩阵操做,让其适应给定的矩形区域就行了。编码

那么第一步的思路超很少就想好了,咱们作到了如何在一个View中排列多张图片,接下来要思考如何分割外围的矩形(View的边界矩形)。spa

咱们知道Android内置了Rect类,用上下左右四个坐标肯定一个矩形,一个大的矩形能够很容易的分为许多小的矩形,相似这样

rect

一个大的矩形被分为三个小矩形。可是这个内置的Rect类真的能帮助咱们完成效果吗?

答案是不能的,虽然内置的Rect类能够成功帮助咱们肯定每张图片的位置,令图片被画在正确的位置上,可是有一点致命的是,它内部是由上下左右四个坐标肯定的,仔细看咱们要实现的效果,在随着咱们手指对矩形边线的移动,大矩形内的小矩形大小边界是在改变的,并且收到影响的矩形确定大于等于2个,那么咱们要改变坐标的矩形也就会大于等于2个,编码上会复杂且容易出错,因此咱们不能单单只用Rect类来肯定边界。咱们必须在抽象出一种新的模型来肯定图片的矩形区域并方便数据更新变化。

在反复把玩Layout for Instagram后(由于当时我还没作出这个View,一直拿Layout研究,但愿你也能够去多玩一下),并把它的全部布局都在纸上画了一遍,我发现了很关键的一点,也是这个自定义View最关键的一部。它的线很重要(当咱们点击其中一张图片后,它会成为选中状态,那个线是高亮的,引人注意哦),咱们每次移动的时那一根线,而一个矩形能够被一根直线或横线划分红两个矩形,而四根线能够肯定一个矩形范围,两个矩形能够共享一根线,线的位置改变,共享这根线的全部矩形的大小范围都会改变。相似这样

  • line1,line2,line4,line5组成了Rect1
  • line2,line3,line4,line5组成了Rect2
  • Rect1和Rect2共享line2,line4,line5
  • 移动了line2后,Rect1和Rect2均收到影响

但愿你们理解这幅图,这是本次自定义View的关键。

那么整理一下大体思路,咱们要用线将View的边界分红许多个小矩形,并让图片画在这些小矩形上,以后同上一篇文章一致,根据咱们的手势控制对应图片的Matrix来控制图片的相应动做。

创建模型

既然思路已经肯定了,那么咱们就要来肯定咱们的代码结构和相应的模型类。上面讲咱们要用线来分割矩形,而Android原生是没有Line这个模型类的,因而咱们要本身抽象一个。那么线是怎么组成的呢?很简单,在坐标系中,两点肯定一根直线,因此咱们要有两个点PointF,由于咱们只用横线或直线,因此只抽象了两个方向,斜线不考虑(本效果只须要直线和横线)。

public class Line {

    public enum Direction {
        HORIZONTAL,
        VERTICAL
    }

    /** * for horizontal line, start means left, end means right * for vertical line, start means top, end means bottom */
    final PointF start;
    final PointF end;

    private Direction direction = Direction.HORIZONTAL;
      ……
}复制代码

可是这么几个属性真的够用吗?在我试验了以后发现是不够的,咱们还须要另外四个属性,是四根其余的线,两根肯定其移动范围的线,两根顶点依附的线,当依附的线移动了后,能够快速更新自身的长度,相应地延长或缩短。

因而咱们Line的模型类就能够去肯定了。

public class Line {

    public enum Direction {
        HORIZONTAL,
        VERTICAL
    }

    /** * for horizontal line, start means left, end means right * for vertical line, start means top, end means bottom */
    final PointF start;
    final PointF end;

    private Direction direction = Direction.HORIZONTAL;

    private Line attachLineStart;
    private Line attachLineEnd;

    private Line mUpperLine;
    private Line mLowerLine;
    ……
}复制代码

那么咱们就能够肯定一个边界Border类,它由4条Line构成,并可方便的导出Rect对象方便咱们摆放图片。

class Border {
    Line lineLeft;
    Line lineTop;
    Line lineRight;
    Line lineBottom;
    ……
}复制代码

接下来就要思考如何支持多样化布局,固然要提供接口供使用者自定义,因此咱们要抽象出一个拼图布局类PuzzleLayout,这个类要有个抽象方法支持咱们自定义布局,并提供一些简单的方法帮助咱们快速布局,而且应该保有全部的边界BorderLine对象,方便进行管理和更新信息。

public abstract class PuzzleLayout {
    ……
    private Border mOuterBorder;

    private List<Border> mBorders = new ArrayList<>();
    private List<Line> mLines = new ArrayList<>();
    private List<Line> mOuterLines = new ArrayList<>(4);
      ……
    public abstract void layout();
      ……
}复制代码

至于图片对象,同上一篇文章同样,每张图片须要一个Matrix对象进行控制,只是在这之上还要保有一个边界Border的引用。这里就不贴了。

这样,咱们全部的模型就已经肯定了。大体关系就是,每一个PuzzleView的布局方式由PuzzleLayout决定,PuzzleLayout可自定义布局,由一系列的边界Border组成,而Border则由一系列的Line组成。

具体实现

因为许多东西的关键都是思路和建模,你们理解了这个思路并创建了正确方便的模型后,实现起来就异常容易了,只是在预约的轨道上开车到终点就行了,其实后面的内容已经不重要了。

布局方式的肯定

起初,咱们要先把布局方式肯定才能够决定画多少张图片上去,因此布局方式是最早要被解决的功能。

你们都知道,一根直线能够把一个矩形分红左右两个矩形,一根横线能够把一个矩形分红上下两个矩形,因此咱们能够提供一个addLine()方法提供分割布局,将增长的LineBorder添加至集合。

protected List<Border> addLine(Border border, Line.Direction direction, float ratio) {
    mBorders.remove(border);
    Line line = BorderUtil.createLine(border, direction, ratio);
    mLines.add(line);

    List<Border> borders = BorderUtil.cutBorder(border, line);
    mBorders.addAll(borders);

    updateLineLimit();
    Collections.sort(mBorders, mBorderComparator);

    return borders;
}复制代码

固然只有这么一个方法布局仍是不怎么方便的哈,因此我还添加了许多方法方便布局,好比一个十字能够把一个矩形分割成四个矩形,一个螺旋能够把一个矩形分割成五个矩形。提供的方法大体就以下图所示

举个例子:

@Override
public void layout() {
    addLine(getOuterBorder(), Line.Direction.VERTICAL, 1f / 2);
    cutBorderEqualPart(getBorder(1), 4, Line.Direction.HORIZONTAL);
    cutBorderEqualPart(getBorder(0), 3, Line.Direction.HORIZONTAL);
}复制代码

以后咱们看一下这种布局分割的效果

图片位置的确立与放置

到这里,咱们已经能够自定义各类各样的布局了,一个View已经被咱们分割成了许多小的矩形区域,接下来咱们就要把图片给画上去,但不是随便画,咱们须要让图片在对应的矩形以centerCrop的方式显示,否则咱们看到的就不是图片的重要区域。那么怎么样才能够作到呢?因为每一个矩形的位置咱们都是知道的,因此咱们只须要将图片的中心移动到对应矩阵的中心,按centerCrop的缩放规则让图片中心缩放就行了。这些就是Matrix的基本应用了,这里就不重复说明了,至于centerCrop的缩放比也很好计算,不会的话,看一下ImageView的源码就行了。

下面的代码是生成让图片已对应Border正确显示的Matrix生成

static Matrix createMatrix(Border border, int width, int height, float extraSize) {
        final RectF rectF = border.getRect();

        Matrix matrix = new Matrix();

        float offsetX = rectF.centerX() - width / 2;
        float offsetY = rectF.centerY() - height / 2;

        matrix.postTranslate(offsetX, offsetY);

        float scale;

        if (width * rectF.height() > rectF.width() * height) {
            scale = (rectF.height() + extraSize) / height;
        } else {
            scale = (rectF.width() + extraSize) / width;
        }

        matrix.postScale(scale, scale, rectF.centerX(), rectF.centerY());

    return matrix;
}复制代码

将图片画上去后的效果,是否是效果很好呀?

图片移动旋转缩放翻转

这个功能和上一篇所讲的方法一致,在onTouchEvent()中监听不一样的手势,对对应图片的Matrix作出相关操做便可,这里就不重复说明了,比较基础。

线的移动

看效果图,这个布局并非不变的,咱们能够经过对可移动线的移动,可使一些边界变大,另外一些边界变小,同时令图片适应边界的变化。这时候模型的正确创建就大大地简化了咱们的编码效率。

首先,咱们找到咱们是否触摸在线上,由于内部的线对象必然会被2个以上的边界引用,当这条线的信息改变时,对应的边界也会立刻得知,并改变其边界区域,这样咱们就能够很方便的从新画出边界,咱们就只要更新受影响区域图片的Matrix便可。

moveLine(event); //移动线
mPuzzleLayout.update(); //更新PuzzleLayout内Border信息
updatePieceInBorder(event); //更新图片Matrix信息以适应变化复制代码

图片位置交换

图片之间的相对位置是能够改变的,按照正常的逻辑也是当咱们长按一张图片时,那张图片会悬浮,而后移动到要交换位置的图片,释放手指就交换成功了。那么问题就是这个悬浮起来的效果,这里用全图显示加个半透明来表示,利用Canvas的相关方法实现及其容易。

if (mHandlingPiece != null && mCurrentMode == Mode.SWAP) {
    mHandlingPiece.draw(canvas, mBitmapPaint, 128);
    if (mReplacePiece != null) {
        drawSelectedBorder(canvas, mReplacePiece);
    }
}复制代码

图片翻转

这个一样利用Matrix能够轻松实现,不赘述。

matrix.postScale(-1, 1, px, py); //水平翻转
matrix.postScale(1, -1, px, py); //垂直翻转复制代码

尾声

到这里,咱们所要实现的功能已经基本所有实现,剩下的就是完善细节,应该提供怎么样的接口供外部操做,只须要慢慢调试便可,感兴趣的同窗能够去看一下源码

总结

此次自定义的View相对于上一次的StickerView来讲,无疑是复杂了不少,咱们须要创建更复杂的模型,可是所运用的核心类是同样的,CanvasMatrix类,同上一篇同样,我仍是要强调思考与建模的重要性,万事开头难,前期的思考无疑是最难的,也占据了整个项目大部分的时间(我花了两周思考,呜呜,可能我太笨了)。

但愿阅读完这篇文章后,能够对你有一些帮助,有什么问题或不懂能够随时联系我,欢迎骚扰。

最近闲下来了,写点文章记录以前的学习并巩固个人基础知识,但愿同你们一块儿进步!

相关文章
相关标签/搜索