Android开发之仿微博贴纸效果实现——基础篇

之前写过一篇关于图像变换处理的文章《Android开发之图像处理那点事——变换》,学以致用,这次我们来实现仿微博的贴纸效果,我打算分成两部分来写:

基础篇:单图贴纸效果,利用矩阵变化+手势识别实现贴纸的自由缩放、旋转、平移,以面向过程的代码让你知道每一步操作的实现原理。

强化篇:仿美图秀秀的多图贴纸效果,以面向对象的思维告诉你如何将图像、矩阵封装,包括贴纸的聚焦处理、重叠场景的交互分析、图像的二次采样、合成等知识点。

关于矩阵的基础操作这里就不再重复阐述,不了解矩阵的朋友可以看一下《Android开发之图像处理那点事——变换》

先来看下本篇文章要实现的效果图:

仿微博贴纸效果
仿微博贴纸效果

实现思路:

我们可以将上述效果大致分成2部分理解,贴图的展示、手势的操作:

贴图的展示:
首先它是一个可以显示图像的自定义View,且它的大小,角度,扭曲程度都是由Matrix矩阵来维护的,所以我们很自然的可以想到 Canvas.drawBitmap(Bitmap, Matrix, Paint)这个绘制方法。

手势的操作:
关于手势的操作,我们大致可以分成这三种,单指的拖动平移,双指的放大缩小,双指的旋转,这里我们需要先了解onTouch中MotionEvent给我们返回的几种事件:
ACTION_DOWN:当手指触摸屏幕的时候触发。
ACTION_MOVE:当手指滑动屏幕的时候触发。
ACTION_UP:当手指抬起的时候触发(此时屏幕无手指触摸)。
ACTION_POINTER_DOWN:当多根手指触摸屏幕的时候触发。
ACTION_POINTER_UP:在多根手机触摸屏幕的情况下,抬起其中一根手指的时候触发。

根据以上的触发事件,我们就可以得到一些我们想要的场景了,比如当单指触摸贴纸的时候,我们将贴纸的属性设为可拖动(不可缩放、旋转),当双指触摸贴纸的时候,我们将贴纸的属性设为不可拖动(可缩放、旋转),而缩放因子我们可以通过双指间距离的改变得到,旋转角度我们可以通过双指移动形成的夹角得到,这些下文会具体分析,先大致有个思路就行。

好了,既然有了思路,我们就开始撸码吧~

编码实现:

首先我们需要将图片加载成Bitmap,并用矩阵Matrix去维护它,在自定义View中画出来:

 canvas.drawBitmap(mBitmap, mMatrix, null);

这里定义三种标志,分别表示当前贴纸处于可移动、可缩放、可旋转状态:

    private boolean mCanTranslate;//标志是否可移动
    private boolean mCanScale;//标志是否可缩放
    private boolean mCanRotate;//标志是否可旋转

下面我们来分别实现贴纸的移动,缩放,旋转效果:

贴纸的移动:

思路:当用户手指(单指)按下屏幕的时候,需要判断手指的触摸点是否在贴纸上,如果在,将贴纸的状态标记为可移动并记录下当前坐标,当用户手指滑动屏幕的时候,需要计算出当前手指所在坐标与刚才按下屏幕坐标的相对距离,通过维护贴纸的矩阵来做平移操作。

代码实现:在onTouch的ACTION_DOWN中去记录触摸点并判断手指触摸点是否在贴纸上,如果在,把状态标记为可移动:

         case MotionEvent.ACTION_DOWN:
                mLastSinglePoint.set(event.getX(), event.getY());
                if (isInStickerView(event)) {
                    //触摸点是否在贴纸范围内
                    mCanTranslate = true;
                }
                mCanScale = false;
                mCanRotate = false;
                break;

检测触摸点的方法,这里简单介绍下Matrix类中的两个方法:

invert:Matrix类中给我们提供了invert方法用来反转矩阵,举个例子,一个向左旋转30°的矩阵。通过invert可以得到一个基于当前(左旋转30°的矩阵)向右旋转30°的矩阵。

mapPoints:Matrix类中给我们提供了mapPoints方法用来映射所有坐标点经过矩阵变化后的新坐标点位置。

有了上面的2个方法,我们就可以根据当前的矩阵得到它变换之前的原矩阵,然后再把当前触摸的点通过原矩阵映射回原来触摸的点,再判断触摸点是否在原来贴纸的矩形框范围内即可。

    /**
     * 检测当前触摸是否在贴纸上
     *
     * @return
     */
    private boolean isInStickerView(MotionEvent motionEvent) {

        if (motionEvent.getPointerCount() == 1) {
            float[] dstPoints = new float[2];
            float[] srcPoints = new float[]{motionEvent.getX(), motionEvent.getY()};
            Matrix matrix = new Matrix();
            mMatrix.invert(matrix);
            matrix.mapPoints(dstPoints, srcPoints);
            if (mBitmapBound.contains(dstPoints[0], dstPoints[1])) {
                return true;
            }
        }

        if (motionEvent.getPointerCount() == 2) {
            float[] dstPoints = new float[4];
            float[] srcPoints = new float[]{motionEvent.getX(0), motionEvent.getY(0), motionEvent.getX(1), motionEvent.getY(1)};
            Matrix matrix = new Matrix();
            mMatrix.invert(matrix);
            matrix.mapPoints(dstPoints, srcPoints);
            if (mBitmapBound.contains(dstPoints[0], dstPoints[1]) || mBitmapBound.contains(dstPoints[2], dstPoints[3])) {
                return true;
            }
        }
        return false;
    }

在onTouch的ACTION_MOVE中去计算x,y相对移动的坐标,然后调用矩阵的平移方法即可:

          case MotionEvent.ACTION_MOVE:
                if (mCanTranslate) {
                    translate(event.getX() - mLastSinglePoint.x, event.getY() - mLastSinglePoint.y);
                    mLastSinglePoint.set(event.getX(), event.getY());
                }
              break;
/**
     * 平移操作
     *
     * @param dx
     * @param dy
     */
    private void translate(float dx, float dy) {
        mMatrix.postTranslate(dx, dy);
        mMatrix.mapPoints(mDstPoints, mScrPoints);
    }

贴纸的缩放:

思路:当用户手指(双指)按下屏幕的时候,需要判断手指的触摸点是否在贴纸上,如果在,将贴纸的状态标记为可缩放并记录下手指之间的距离,当用户手指滑动屏幕的时候,我们可以计算出当前手指之间的距离与刚才按下屏幕手指间距离的比值,这个比值就是贴纸的缩放因子,大于1表示放大,小于1表示缩小,缩放中心为贴纸中心。

代码实现:在onTouch的ACTION_POINTER_DOWN中去判断手指触摸点的个数和触摸点位置,如果触摸点为2且在贴纸上,将状态标记为可缩放,并记录下手指间的距离:

          case MotionEvent.ACTION_POINTER_DOWN:
                if (event.getPointerCount() == 2 && isInStickerView(event)) {
                    mCanTranslate = false;
                    mCanScale = true;
                    mCanRotate = true;
                    //计算双指之间向量
                    mLastDistancePoint.set(event.getX(0) - event.getX(1), event.getY(0) - event.getY(1));
                    //计算双指之间距离
                    mLastDistance = calculateDistance(event);
                }
                break;

根据直角三角形勾股定理可以得到手指间的距离:

 /**
     * 计算两点之间的距离
     */
    private float calculateDistance(MotionEvent event) {
        float x = event.getX(0) - event.getX(1);
        float y = event.getY(0) - event.getY(1);
        return (float) Math.sqrt(x * x + y * y);
    }

在onTouch的ACTION_MOVE中得到新的手指间的距离,与刚才手指按下屏幕时记录的距离做对比得到缩放因子,然后调用矩阵的缩放方法即可,缩放中心为贴纸中点:

            case MotionEvent.ACTION_MOVE:
                if (mCanScale && event.getPointerCount() == 2) {
                    //操作自由缩放
                    //手指间距离
                    float distance = calculateDistance(event);
                    //根据双指移动的距离获取缩放因子
                    float scale = distance / mLastDistance;
                    scale(scale, scale, getMidPoint().x, getMidPoint().y);
                    mLastDistance = distance;
                }
                break;
   /**
     * 缩放操作
     *
     * @param sx
     * @param sy
     * @param px
     * @param py
     */
    private void scale(float sx, float sy, float px, float py) {
        mMatrix.postScale(sx, sy, px, py);
        mMatrix.mapPoints(mDstPoints, mScrPoints);
    }

贴纸的旋转:

思路:当用户手指(双指)按下屏幕的时候,需要判断手指的触摸点是否在贴纸上,如果在,将贴纸的状态标记为可旋转并记录下手指所形成的向量,当用户手指滑动屏幕的时,需要计算出当前手指所形成的向量与刚才按下屏幕手指所形成的向量的角度差,这个角度差就是贴纸应该旋转的角度了,旋转中心为贴纸中点:

代码实现:在onTouch的ACTION_POINTER_DOWN中去判断手指触摸点的个数和触摸点位置,如果触摸点在贴纸上,将状态标记为可旋转并记录下手指间的所形成的向量:

          case MotionEvent.ACTION_POINTER_DOWN:
                if (event.getPointerCount() == 2 && isInStickerView(event)) {
                    mCanTranslate = false;
                    mCanScale = true;
                    mCanRotate = true;
                    //计算双指之间向量
                    mLastDistancePoint.set(event.getX(0) - event.getX(1), event.getY(0) - event.getY(1));
                    //计算双指之间距离
                    mLastDistance = calculateDistance(event);
                }
                break;

在onTouch的ACTION_MOVE中得到新的手指间所形成的向量,然后去计算它们之间所形成的夹角值:

           case MotionEvent.ACTION_MOVE:
                if (mCanRotate && event.getPointerCount() == 2) {
                    //操作自由旋转
                    mDistancePoint.set(event.getX(0) - event.getX(1), event.getY(0) - event.getY(1));
                    rotate(calculateDegrees(mLastDistancePoint, mDistancePoint), getMidPoint().x, getMidPoint().y);
                    mLastDistancePoint.set(mDistancePoint.x, mDistancePoint.y);
                }
                break;

我们在这里可以通过计算向量的斜率差来获取手指间的旋转角度:

    /**
     * 计算旋转角度
     *
     * @param lastPoint
     * @param pointF
     * @return
     */
    private float calculateDegrees(PointF lastPoint, PointF pointF) {
        float lastDegrees = (float) Math.atan2(lastPoint.y, lastPoint.x);
        float currentDegrees = (float) Math.atan2(pointF.y, pointF.x);
        return (float) Math.toDegrees(currentDegrees - lastDegrees);
    }
   /**
     * 旋转操作
     *
     * @param degrees
     * @param px
     * @param py
     */
    private void rotate(float degrees, float px, float py) {
        mMatrix.postRotate(degrees, px, py);
        mMatrix.mapPoints(mDstPoints, mScrPoints);
    }

以上就是实现贴纸效果的核心代码了,很简单吧,其实就只是矩阵、手势、三角函数的综合运用。

补充说明:

1、细心的朋友会发现在上面的代码中,平移、缩放、旋转操作都伴随着一行代码mMatrix.mapPoints(mDstPoints, mScrPoints);,这句话是做什么用的呢?其实一开始图片加载成Bitmap对象的时候,我就记录下了一些特殊点:

        //初始化图像
        mBitmap = BitmapFactory.decodeResource(context.getResources(), R.mipmap.icon);
        //记录图像一些点位置
        mScrPoints = new float[]{
                0, 0,//左上
                mBitmap.getWidth(), 0,//右上
                mBitmap.getWidth(), mBitmap.getHeight(),//右下
                0, mBitmap.getHeight(),//左下
                mBitmap.getWidth() / 2, mBitmap.getHeight() / 2//中间点
        };
        //拷贝点位置
        mDstPoints = mScrPoints.clone();

然后根据上文介绍mapPoints方法,将每一次矩阵变化所影响的点位置都做了映射,这样我们就可以很方便的得到任一时刻的最新点位置,比如我们要知道某一时刻图片的中点位置,我们就可以这样做:

  /**
   * 获取图像中心点
   *
   * @return
   */
  private PointF getMidPoint() {
      mMidPoint.set(mDstPoints[8], mDstPoints[9]);
      return mMidPoint;
  }

2、关于旋转角度的计算,这边可以有很多方法,上文我采用的是计算出手指间的向量,然后求出他们的斜率差,然后转换成角度,这里额外多介绍一种求角度的方法:
通过余弦定理求夹角:我们以图片的中点为旋转中心,加上我们双指的触碰点,我们就可以知道三角形的三个点坐标,就可以知道三边的距离,通过余弦定理我们很轻松的可以得到cos值,再将其转换成角度即可。
如果不清楚余弦定理的朋友请戳:余弦定理视频讲解

余弦定理

这里需要注意象限问题,也就是cos值的正负,因为旋转有正时针方向和逆时针方向,这里我们可以通过向量积来判断:
向量积

3、在做完一些列手势操作,手指抬起的时候,我们把状态重置:

  /**
     * 重置状态
     */
    private void reset() {
        mCanTranslate = false;
        mCanScale = false;
        mCanRotate = false;
        mLastDistance = 0f;
        mMidPoint.set(0f, 0f);
        mLastSinglePoint.set(0f, 0f);
        mLastDistancePoint.set(0f, 0f);
        mDistancePoint.set(0f, 0f);
    }

好了,到这里文章就结束啦,这里给出完整代码(启蒙思路,优化版请见下一篇文章):

package com.lcw.view;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.PointF;
import android.graphics.RectF;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.view.MotionEvent;
import android.view.View;
import android.view.WindowManager;

import com.lcw.view.R;

/**
 * 自定义贴纸View
 * Create by: chenWei.li
 * Date: 2018/11/22
 * Time: 下午11:02
 * Email: lichenwei.me@foxmail.com
 */

/**
 * @Deprecated 基础贴纸类,废弃不再使用
 */
public class StickerView extends View implements View.OnTouchListener {

    private Bitmap mBitmap;//贴纸图片

    private Matrix mMatrix;//维护图像变化的矩阵
    private float[] mScrPoints;//矩阵变换前的点坐标
    private float[] mDstPoints;//矩阵变换后的点坐标
    private RectF mBitmapBound;//图片的外围边框的点坐标

    private boolean mCanTranslate;//标志是否可移动
    private boolean mCanScale;//标志是否可缩放
    private boolean mCanRotate;//标志是否可旋转

    private float mLastDistance;//记录上一次双指之间的距离
    private PointF mMidPoint = new PointF();//记录图片中心点
    private PointF mLastSinglePoint = new PointF();//记录上一次单指触摸屏幕的点坐标
    private PointF mLastDistancePoint = new PointF();//记录上一次双指触摸屏幕的点坐标
    private PointF mDistancePoint = new PointF();//记录当前双指触摸屏幕的点坐标

    private Paint mPaint;


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

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

    public StickerView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context);
    }

    /**
     * 完成一些初始化操作
     *
     * @param context
     */
    private void init(Context context) {

        //初始化画笔
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setColor(Color.GRAY);

        //初始化图像
        mBitmap = BitmapFactory.decodeResource(context.getResources(), R.mipmap.icon);
        //记录图像一些点位置
        mScrPoints = new float[]{
                0, 0,//左上
                mBitmap.getWidth(), 0,//右上
                mBitmap.getWidth(), mBitmap.getHeight(),//右下
                0, mBitmap.getHeight(),//左下
                mBitmap.getWidth() / 2, mBitmap.getHeight() / 2//中间点
        };
        //拷贝点位置
        mDstPoints = mScrPoints.clone();
        mBitmapBound = new RectF(0, 0, mBitmap.getWidth(), mBitmap.getHeight());

        //初始化矩阵
        mMatrix = new Matrix();

        //移动图像到屏幕中心
//        WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
//        DisplayMetrics displayMetrics = new DisplayMetrics();
//        windowManager.getDefaultDisplay().getMetrics(displayMetrics);
//        float dx = displayMetrics.widthPixels / 2 - mBitmap.getWidth() / 2;
//        float dy = displayMetrics.heightPixels / 2 - mBitmap.getHeight() / 2;
//        translate(dx, dy);

        //设置触摸监听
        setOnTouchListener(this);

    }


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


    @Override
    public boolean onTouch(View v, MotionEvent event) {
        int action = event.getAction();
        switch (action & MotionEvent.ACTION_MASK) {
            case MotionEvent.ACTION_DOWN:
                mLastSinglePoint.set(event.getX(), event.getY());
                if (isInStickerView(event)) {
                    //触摸点是否在贴纸范围内
                    mCanTranslate = true;
                }
                mCanScale = false;
                mCanRotate = false;
                break;
            case MotionEvent.ACTION_POINTER_DOWN:
                if (event.getPointerCount() == 2 && isInStickerView(event)) {
                    mCanTranslate = false;
                    mCanScale = true;
                    mCanRotate = true;
                    //计算双指之间向量
                    mLastDistancePoint.set(event.getX(0) - event.getX(1), event.getY(0) - event.getY(1));
                    //计算双指之间距离
                    mLastDistance = calculateDistance(event);
                }
                break;
            case MotionEvent.ACTION_MOVE:
                if (mCanTranslate) {
                    translate(event.getX() - mLastSinglePoint.x, event.getY() - mLastSinglePoint.y);
                    mLastSinglePoint.set(event.getX(), event.getY());
                }
                if ((mCanScale || mCanRotate) && event.getPointerCount() == 2) {
                    //操作自由缩放
                    float distance = calculateDistance(event);
                    //根据双指移动的距离获取缩放因子
                    float scale = distance / mLastDistance;
                    scale(scale, scale, getMidPoint().x, getMidPoint().y);
                    mLastDistance = distance;
                    //操作自由旋转
                    mDistancePoint.set(event.getX(0) - event.getX(1), event.getY(0) - event.getY(1));
                    rotate(calculateDegrees(mLastDistancePoint, mDistancePoint), getMidPoint().x, getMidPoint().y);
                    mLastDistancePoint.set(mDistancePoint.x, mDistancePoint.y);
                }
                break;
            case MotionEvent.ACTION_UP:
                reset();
                break;
        }
        invalidate();
        return true;
    }

    /**
     * 平移操作
     *
     * @param dx
     * @param dy
     */
    private void translate(float dx, float dy) {
        mMatrix.postTranslate(dx, dy);
        mMatrix.mapPoints(mDstPoints, mScrPoints);
    }

    /**
     * 缩放操作
     *
     * @param sx
     * @param sy
     * @param px
     * @param py
     */
    private void scale(float sx, float sy, float px, float py) {
        mMatrix.postScale(sx, sy, px, py);
        mMatrix.mapPoints(mDstPoints, mScrPoints);
    }

    /**
     * 旋转操作
     *
     * @param degrees
     * @param px
     * @param py
     */
    private void rotate(float degrees, float px, float py) {
        mMatrix.postRotate(degrees, px, py);
        mMatrix.mapPoints(mDstPoints, mScrPoints);
    }


    /**
     * 检测当前触摸是否在贴纸上
     *
     * @return
     */
    private boolean isInStickerView(MotionEvent motionEvent) {

        if (motionEvent.getPointerCount() == 1) {
            float[] dstPoints = new float[2];
            float[] srcPoints = new float[]{motionEvent.getX(), motionEvent.getY()};
            Matrix matrix = new Matrix();
            mMatrix.invert(matrix);
            matrix.mapPoints(dstPoints, srcPoints);
            if (mBitmapBound.contains(dstPoints[0], dstPoints[1])) {
                return true;
            }
        }

        if (motionEvent.getPointerCount() == 2) {
            float[] dstPoints = new float[4];
            float[] srcPoints = new float[]{motionEvent.getX(0), motionEvent.getY(0), motionEvent.getX(1), motionEvent.getY(1)};
            Matrix matrix = new Matrix();
            mMatrix.invert(matrix);
            matrix.mapPoints(dstPoints, srcPoints);
            if (mBitmapBound.contains(dstPoints[0], dstPoints[1]) || mBitmapBound.contains(dstPoints[2], dstPoints[3])) {
                return true;
            }
        }
        return false;
    }


    /**
     * 获取图像中心点
     *
     * @return
     */
    private PointF getMidPoint() {
        mMidPoint.set(mDstPoints[8], mDstPoints[9]);
        return mMidPoint;
    }

    /**
     * 计算两点之间的距离
     */
    private float calculateDistance(MotionEvent event) {
        float x = event.getX(0) - event.getX(1);
        float y = event.getY(0) - event.getY(1);
        return (float) Math.sqrt(x * x + y * y);
    }


    /**
     * 计算旋转角度
     *
     * @param lastPoint
     * @param pointF
     * @return
     */
    private float calculateDegrees(PointF lastPoint, PointF pointF) {
        float lastDegrees = (float) Math.atan2(lastPoint.y, lastPoint.x);
        float currentDegrees = (float) Math.atan2(pointF.y, pointF.x);
        return (float) Math.toDegrees(currentDegrees - lastDegrees);
    }

    /**
     * 重置状态
     */
    private void reset() {
        mCanTranslate = false;
        mCanScale = false;
        mCanRotate = false;
        mLastDistance = 0f;
        mMidPoint.set(0f, 0f);
        mLastSinglePoint.set(0f, 0f);
        mLastDistancePoint.set(0f, 0f);
        mDistancePoint.set(0f, 0f);
    }

}

下一篇:《Android开发之仿微博贴纸效果实现——进阶篇》

源码下载:

这里附上源码地址(欢迎Star,欢迎Fork):StickerView

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容

  • 前言 最近有需求要做一个画布,这个画布以一个图片为背景,可以实现缩放,涂鸦以及贴纸的功能,缩放和涂鸦要兼顾,于是就...
    王岩_shang阅读 7,184评论 8 29
  • 效果图: Github链接:https://github.com/boycy815/PinchImageView ...
    CQ_TYL阅读 2,205评论 0 0
  • 概述 了解过自定义View的童鞋 对Canvas.drawBitmap(Bitmap, Matrix, Paint...
    RazorZ阅读 8,068评论 6 70
  • 1 前言 OpenGL渲染3D模型离不开空间几何的数学理论知识,而本篇文章的目的就是对空间几何进行简单的介绍,并对...
    RichardJieChen阅读 6,909评论 1 11
  • 上班几年,对于幸福的理解开始发生一些小小的变化,也许每个人都有自己的幸福,只不过状态不同而已。 1 刚刚上班的时候...
    知否红瘦阅读 493评论 0 1