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閱讀 228,333評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,491評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,263評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,946評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,708評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,186評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,255評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,409評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,939評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,774評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,976評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,518評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,209評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,641評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,872評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,650評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,958評論 2 373

推薦閱讀更多精彩內容

  • 前言 最近有需求要做一個畫布,這個畫布以一個圖片為背景,可以實現縮放,涂鴉以及貼紙的功能,縮放和涂鴉要兼顧,于是就...
    王巖_shang閱讀 7,314評論 8 29
  • 效果圖: Github鏈接:https://github.com/boycy815/PinchImageView ...
    CQ_TYL閱讀 2,235評論 0 0
  • 概述 了解過自定義View的童鞋 對Canvas.drawBitmap(Bitmap, Matrix, Paint...
    RazorZ閱讀 8,150評論 6 70
  • 1 前言 OpenGL渲染3D模型離不開空間幾何的數學理論知識,而本篇文章的目的就是對空間幾何進行簡單的介紹,并對...
    RichardJieChen閱讀 7,058評論 1 11
  • 上班幾年,對于幸福的理解開始發生一些小小的變化,也許每個人都有自己的幸福,只不過狀態不同而已。 1 剛剛上班的時候...
    知否紅瘦閱讀 519評論 0 1