Android 图片分片过渡效果

前言

在之前的文章中,通过LED效果马赛克效果两篇文章,介绍了分片绘制的效果的方法和原理,通过这两篇文章,相信大家都已经熟悉了分片绘制的思路。其实分片绘制不仅仅能实现LED、马赛克等特殊效果,实际上类似百叶窗、图片对角线锯齿过渡等,很多PPT中存在的特效,基本上也是按照这种原理来实现的。

分片可以有很多种意想不到的效,我们再来说一下分片特点:

  • [1] 按一定的距离、大小、角度对区域进行对一张图片或者区域裁剪或者提取区域图像
  • [2] 对提取出来的区域进行一系列变换,如百叶窗、微信摇一摇等
  • [3] 被裁剪的区域可以还原回去

技术前景

其实单纯的分片可以做一些瓦片效果,当然还可以做一些组合效果,下面是一个github开源项目(Camera2DApplication)利用Camera和图片分片实现的效果,这个过程中对一张图片进行分片绘制。

代码中的逻辑不是很复杂,本质上就是利用2张图片实现的,我们先来看下代码实现,作者的代码很认真,注释都写了,涉及postTranslate比较难懂的操作我也进行了微调。

scss 复制代码
/**
 * 3d旋转效果
 *
 * @param canvas
 */
private void drawModeNormal(Canvas canvas) {
    //VERTICAL时使用rotateY,HORIZONTAL时使用rotateX
    if (orientation == VERTICAL) {
        //如果是前进,则画当前图,后退则画上一张图,注释用的是前进情况
        matrix.reset();
        camera.save();
        //旋转角度 0 - -maxDegress 
        camera.rotateX(-degress);
        camera.getMatrix(matrix);
        camera.restore();

        //绕着图片top旋转
        matrix.preTranslate(-viewWidth / 2f, 0);
        //旋转轴向下平移,则图片也向下平移
        matrix.postTranslate(viewWidth / 2f, rotatePivotY);
        //如果是前进,则画当前图,后退则画上一张图,因为后退时,这里画的是动画下方出来的图片,而下方的图片是前一张图
        canvas.drawBitmap(getBitmapScale(bitmapResourceIds.get(isForward ? currentIndex : preIndex), viewWidth, viewHeight),
                matrix, mPaint);

        //在处理下一张图片
        matrix.reset();
        camera.save();
        //旋转角度 maxDegress - 0
        camera.rotateX(maxDegress - degress);
        camera.getMatrix(matrix);
        camera.restore();

        //绕着图片bottom旋转
        matrix.preTranslate(-viewWidth / 2f, -viewHeight);
        //旋转轴向下平移,则图片也向下平移
        matrix.postTranslate(viewWidth / 2f, rotatePivotY);
        //如果是前进,则画下一张图,后退则画当前图,后退时,这边代码画的是动画上方的图片,上方的图片是当前图片
        canvas.drawBitmap(getBitmapScale(bitmapResourceIds.get(isForward ? nextIndex : currentIndex), viewWidth, viewHeight),
                matrix, mPaint);
    } else {
        //如果是前进,则画当前图,后退则画上一张图,注释用的是前进情况
        matrix.reset();
        camera.save();
        //旋转角度 0 - maxDegress 
        camera.rotateY(degress);
        camera.getMatrix(matrix);
        camera.restore();

        //绕着图片left旋转
        matrix.preTranslate(0, -viewHeight / 2);
        //旋转轴向右平移,则图片也向右平移
        matrix.postTranslate(rotatePivotX, viewHeight / 2);
        //如果是前进,则画当前图,后退则画上一张图,因为后退时,这里画的是动画右方出来的图片,而右方的图片是前一张图
        canvas.drawBitmap(getBitmapScale(bitmapResourceIds.get(isForward ? currentIndex : preIndex), viewWidth, viewHeight),
                matrix, mPaint);

        //在处理下一张图片
        matrix.reset();
        camera.save();
        //旋转角度 -maxDegress - 0
        camera.rotateY(-maxDegress + degress);
        camera.getMatrix(matrix);
        camera.restore();

        //绕着图片right旋转
        matrix.preTranslate(-viewWidth, -viewHeight / 2f);
        //旋转轴向右平移,则图片也向右平移
        matrix.postTranslate(rotatePivotX, viewHeight / 2f);
        //如果是前进,则画下一张图,后退则画当前图,后退时,这边代码画的是动画左方的图片,左方的图片是当前图片
        canvas.drawBitmap(getBitmapScale(bitmapResourceIds.get(isForward ? nextIndex : currentIndex), viewWidth, viewHeight),
                matrix, mPaint);
    }
}

分片操作

下面是分片操作,这个地方其实可以不用创建Bitmap缓存,创建Path就行,绘制时对Path区域利用Shader贴图即可。

arduino 复制代码
private Bitmap getBitmapScale(int resId, float width, float height) {
    if (ImageCache.getInstance().getBitmapFromMemCache(String.valueOf(resId)) != null) {
        return ImageCache.getInstance().getBitmapFromMemCache(String.valueOf(resId));
    }
    Bitmap bitmap = BitmapFactory.decodeResource(getResources(), resId);
    //创建分片
    Bitmap bitmapDst = Bitmap.createScaledBitmap(bitmap, (int) width, (int) height, false);
    bitmap.recycle();

    ImageCache.getInstance().addBitmapToMemoryCache(String.valueOf(resId)
            , bitmapDst);
    return bitmapDst;
}

小试一下

我们这里通过一个简单的Demo,实现一种特效,这次我们利用网格矩阵分片。说到矩阵,很多人面试的时候都会遇到一些算法题,比较幸运的人遇到的是矩阵旋转90度、逆时针打印矩阵、矩阵孤岛问题、从左上角开始进行矩阵元素搜索,运气稍差的会遇到由外到里顺时针打印矩阵和斜对角打印矩阵,后面两种看似简单的问题实际上做起来并不顺手,有点扯远了,我们来看看效果。

你没看错,这次遇到了算法问题,我这边用的空间换取时间的方法。

图像分片

将图片分片,计算出网格的列和行

sql 复制代码
int col = (int) Math.ceil(mBitmaps[index].getWidth() / blockWidth);
int row = (int) Math.ceil(mBitmaps[index].getHeight() / blockWidth);

分片算法

这个算法实际上是每次将列数 +1,然后按对角分割,把符合的区域添加到path中

scss 复制代码
int x = xPosition;
int y = 0;
while (x >= 0 && y <= row) {
    if (x < col && y < row) {
        dstRect.set((int) (x * blockWidth), (int) (y * blockWidth), (int) (x * blockWidth + blockWidth), (int) (y * blockWidth + blockWidth));
      //  bitmapCanvas.drawBitmap(mBitmaps[index], dstRect, dstRect, mCommonPaint);
        path.addRect(dstRect, Path.Direction.CCW);  //加入网格分片
    }
    x--;
    y++;
}

Path 路径贴图

  • Path过程中我们添加的rect是闭合区域,是可以贴图的,当然,一般有三种方法:
  • Path的贴图一般使用 clipPath对图片裁剪然后贴图,当然还有将对应的图片区域绘制到View上
  • Path 是Rect,按照Rect将图片区域绘制到Rect区域
  • 使用BitmapShader一次性绘制

实际上我们应该尽可能使用Bitmap,因为BitmapShader唯一是不存在锯齿性能比较好的绘制方法。

ini 复制代码
int save = bitmapCanvas.save();
mCommonPaint.setShader(new BitmapShader(mBitmaps[index], Shader.TileMode.CLAMP, Shader.TileMode.CLAMP));
bitmapCanvas.drawPath(path,mCommonPaint);
bitmapCanvas.restoreToCount(save);

其实我们的核心代码到这里就结束了,我们可以看到,分片可以的意义很重要的,当然,借助其他工具也可以实现,不过代码实现的好处是可以编辑和交互,不是所有的动画都可以产生交互。

到此,我们还可以对今天的demo添加一些想象

  • 从中间外扩效果
  • 奇偶行切换效果
  • 国际象棋黑白格子变换效果
  • ......

总结

这是我们的第三篇关于图片分片特效的博客,希望通过一些了的文章,熟悉一些技术,往往看似高大上的效果,其实就是通过普普通通的方法叠加在一起的,当然,让你的技术承载你的想象,才是最重要的。

本篇demo全部代码

实际上代码贴太多很可能没人看,但是依照惯例,我们给出完整代码。

ini 复制代码
public class TilesView extends View {
    private final DisplayMetrics mDM;
    private TextPaint mCommonPaint;
    private RectF mainRect = new RectF();
    private BitmapCanvas bitmapCanvas; //Canvas 封装的
    private Bitmap[] mBitmaps;
    private RectF dstRect = new RectF();
    Path path = new Path();
    private float blockWidth = 50f;
    private int xPosition = -2;
    private int index = 0;
    private boolean isTicking = false;
    public TilesView(Context context) {
        this(context, null);
    }
    public TilesView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public TilesView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mDM = getResources().getDisplayMetrics();
        initPaint();
    }

    private void initPaint() {
        //否则提供给外部纹理绘制
        mCommonPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
        mCommonPaint.setAntiAlias(true);
        mCommonPaint.setStyle(Paint.Style.FILL_AND_STROKE);
        mCommonPaint.setStrokeCap(Paint.Cap.ROUND);
        mCommonPaint.setFilterBitmap(true);
        mCommonPaint.setDither(true);
        mBitmaps = new Bitmap[3];
        mBitmaps[0] = decodeBitmap(R.mipmap.mm_013);
        mBitmaps[1] = decodeBitmap(R.mipmap.mm_014);
        mBitmaps[2] = decodeBitmap(R.mipmap.mm_015);
    }

    private Bitmap decodeBitmap(int resId) {
        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inMutable = true;
        return BitmapFactory.decodeResource(getResources(), resId, options);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);

        if (widthMode != MeasureSpec.EXACTLY) {
            widthSize = mDM.widthPixels / 2;
        }
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        if (heightMode != MeasureSpec.EXACTLY) {
            heightSize = widthSize / 2;
        }
        setMeasuredDimension(widthSize, heightSize);

    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        if (bitmapCanvas != null && bitmapCanvas.bitmap != null && !bitmapCanvas.bitmap.isRecycled()) {
            bitmapCanvas.bitmap.recycle();
        }
        bitmapCanvas = null;

    }


    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        int width = getWidth();
        int height = getHeight();
        if (width < 1 || height < 1) {
            return;
        }
        if (bitmapCanvas == null || bitmapCanvas.bitmap == null || bitmapCanvas.bitmap.isRecycled()) {
            bitmapCanvas = new BitmapCanvas(Bitmap.createBitmap(mBitmaps[index].getWidth(), mBitmaps[index].getHeight(), Bitmap.Config.ARGB_8888));
        }
        int nextIndex = (index + 1) % mBitmaps.length;
        canvas.drawBitmap(mBitmaps[nextIndex],0,0,mCommonPaint);

        int col = (int) Math.ceil(mBitmaps[index].getWidth() / blockWidth);
        int row = (int) Math.ceil(mBitmaps[index].getHeight() / blockWidth);
        mCommonPaint.setStyle(Paint.Style.FILL);

     //   path.reset();
//        for (int x = 0; x < row; x++) {
//            for (int y = 0; y < col; y++) {
//                gridRectF.set(x * blockWidth, y * blockWidth, x * blockWidth + blockWidth, y * blockWidth + blockWidth);
//                canvas.drawRect(gridRectF, mCommonPaint);
//                path.addRect(gridRectF, Path.Direction.CCW);
//            }
//        }

        diagonalEffect(col,row,xPosition,path);
        canvas.drawBitmap(bitmapCanvas.bitmap, 0, 0, mCommonPaint);

        if (isTicking && xPosition >= 0 && xPosition < col * 2) {
            clockTick();
        } else if(isTicking){
            xPosition = -1;
            index = nextIndex;
            isTicking = false;
        }
    }

    private void diagonalEffect(int col, int row, int xPosition,Path path) {
        int x = xPosition;
        int y = 0;
        while (x >= 0 && y <= row) {
            if (x < col && y < row) {
                dstRect.set((int) (x * blockWidth), (int) (y * blockWidth), (int) (x * blockWidth + blockWidth), (int) (y * blockWidth + blockWidth));
              //  bitmapCanvas.drawBitmap(mBitmaps[index], dstRect, dstRect, mCommonPaint);
                path.addRect(dstRect, Path.Direction.CCW);  //加入网格分片
            }
            x--;
            y++;
        }
        int save = bitmapCanvas.save();
        mCommonPaint.setShader(new BitmapShader(mBitmaps[index], Shader.TileMode.CLAMP, Shader.TileMode.CLAMP));
        bitmapCanvas.drawPath(path,mCommonPaint);
        bitmapCanvas.restoreToCount(save);

    }

    public void tick() {
        isTicking = true;
        xPosition = -1;
        path.reset();
        clockTick();
    }

    private void clockTick() {
        xPosition += 1;
        postInvalidateDelayed(16);
    }


    public float dp2px(float dp) {
        return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, mDM);
    }

    public float sp2px(float dp) {
        return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, dp, mDM);
    }

    static class BitmapCanvas extends Canvas {
        Bitmap bitmap;
        public BitmapCanvas(Bitmap bitmap) {
            super(bitmap);
            //继承在Canvas的绘制是软绘制,因此理论上可以绘制出阴影
            this.bitmap = bitmap;
        }

        public Bitmap getBitmap() {
            return bitmap;
        }
    }
}
相关推荐
吾日三省吾码1 小时前
JVM 性能调优
java
Estar.Lee1 小时前
查手机号归属地免费API接口教程
android·网络·后端·网络协议·tcp/ip·oneapi
温辉_xh1 小时前
uiautomator案例
android
y先森2 小时前
CSS3中的伸缩盒模型(弹性盒子、弹性布局)之伸缩容器、伸缩项目、主轴方向、主轴换行方式、复合属性flex-flow
前端·css·css3
前端Hardy2 小时前
纯HTML&CSS实现3D旋转地球
前端·javascript·css·3d·html
susu10830189112 小时前
vue3中父div设置display flex,2个子div重叠
前端·javascript·vue.js
弗拉唐2 小时前
springBoot,mp,ssm整合案例
java·spring boot·mybatis
oi773 小时前
使用itextpdf进行pdf模版填充中文文本时部分字不显示问题
java·服务器
工业甲酰苯胺3 小时前
MySQL 主从复制之多线程复制
android·mysql·adb
IT女孩儿3 小时前
CSS查缺补漏(补充上一条)
前端·css