android圖片裁剪拼接實現(二):觸控實現

Kongdy發表於2019-03-01

前文

android圖片裁剪拼接實現(一):Matrix基本使用中說到了如何自定義控制元件並如何使用Matrix對其進行縮放、旋轉等處理,這次就說說怎麼把這些控制實現到觸控上面。

android觸控機制

首先,當使用者點下螢幕的時候,Linux會將觸控包裝成Event,然後InputReader會收到來自EventBus傳送過來的Event,最後InputDispatcher分發給ViewRootImpl,ViewRootImpl再傳遞給DecorView,這最終才到達了我們的當前介面,接下來的傳遞如下圖所示。

android觸控傳遞


圖畫的不好,水平有限,望見諒。

事件分發


那從這裡我們就知道,我們要寫的view,需要先從dispatchTouchEvent()裡面分發觸控事件,然後再TouchEvent()裡面進行事件的處理。以下是dispatchTouchEvent中的處理。

 @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        // 分發各個img的觸控事件
        if (mViewMode != VIEW_MODE_IDLE && findIndex >= 0) {
            imgList.get(findIndex).onTouchEvent(event);
            return true;
        }
        switch (event.getActionMasked()) {
            case MotionEvent.ACTION_DOWN:
                if (mViewMode == VIEW_MODE_IDLE) {
                    findIndex = findTouchImg(event);
                    if (findIndex >= 0) {
                        return true;
                    }
                }
                break;
            case MotionEvent.ACTION_POINTER_DOWN:
                // 判斷落點是否在img中
                if (mViewMode == VIEW_MODE_IDLE) {
                    findIndex = findTouchImg(event);
                    if (findIndex >= 0) {
                        imgList.get(findIndex).onTouchEvent(event);
                        if (getParent() != null)
                            getParent().requestDisallowInterceptTouchEvent(true);
                        return true;
                    }
                }
                break;
        }
        return false;
    }
複製程式碼


這裡使用getActionMask()是為了更好的處理多點觸控。使用findTouchImg()方法,判斷如果點落到圖片區域就消費這次事件,但是,後續的觸控事件,父控制元件還是有可能攔截的,這次只是消費了這次按壓觸控事件。如果是多點觸控,就直接呼叫requestDisallowInterceptTouchEvent的方法,禁止父控制元件攔截子控制元件的後續事件,不過使用這個方法要記著後面釋放。判斷確實是多點觸控之後,就直接在方法頂部執行Img的方法,避免下面不必要的判斷。這裡findTouchImg()方法主要是根據每個Img的DrawRect進行點的落位判定。方法如下

	 /**
     * @return -1 is not find
     */
    private int findTouchImg(MotionEvent event) {
        final float touchX = event.getX();
        final float touchY = event.getY();
        for (int i = 0; i < imgList.size(); i++) {
            ImageData imageData = imgList.get(i);
            if (imageData.drawRect.contains(touchX, touchY)) {
                return i;
            }
        }
        return -1;
    }
複製程式碼

觸控事件處理

這裡我們主要實現兩種效果,縮放和旋轉。我們把Img的touch處理封裝到了ImageData裡面,程式碼如下:

    /**
         * imageData的觸控處理事件
         *
         * @param e 觸控事件
         */
        protected void onTouchEvent(MotionEvent e) {
            switch (e.getActionMasked()) {
                case MotionEvent.ACTION_DOWN:
                    break;
                case MotionEvent.ACTION_POINTER_DOWN:
                    requestDisallowInterceptTouchEvent(true);
                    distanceStub = getPointDistance(e);
                    angleStub = getPointAngle(e);
                    break;
                case MotionEvent.ACTION_MOVE:
                    // confirm multi touch
                    if (e.getPointerCount() > 1) {
                        float tempDistance = getPointDistance(e);
                        float tempAngle = getPointAngle(e);
                        float tempScale = this.getScale();
                        float tempRotateAngle = this.getRotateAngle();

                        tempScale += (tempDistance / distanceStub) - 1;
                        tempRotateAngle += tempAngle - angleStub;

                        angleStub = tempAngle;
                        distanceStub = tempDistance;

                        this.setRotateAngle(tempRotateAngle);
                        this.setScale(tempScale);
                        reDraw();
                    }
                    break;
                case MotionEvent.ACTION_CANCEL:
                case MotionEvent.ACTION_UP:
                    runAngleAdsorbentAnim(findIndex);
                    requestDisallowInterceptTouchEvent(false);
                    if (getParent() != null)
                        getParent().requestDisallowInterceptTouchEvent(false);
                    distanceStub = 0;
                    angleStub = 0;
                    findIndex = -1;
                    break;
            }
        }
複製程式碼


當多點觸控的時候,記錄下最先兩個觸控點的距離和斜率角度,在隨後發生滑動的時候,計算與之前觸控點距離和斜率角度發生的變化,再對Bitmap進行即時調整。計算距離和斜率角度的方法如下:

private float getPointDistance(MotionEvent e) {
            if (e.getPointerCount() > 1) {
                final float touchX1 = e.getX(0);
                final float touchY1 = e.getY(0);
                final float touchX2 = e.getX(1);
                final float touchY2 = e.getY(1);
                return (float) Math.abs(Math.sqrt(Math.pow(touchX2 - touchX1, 2) + 
                Math.pow(touchY2 - touchY1, 2)));
            }
            return 0;
        }

        private float getPointAngle(MotionEvent e) {
            if (e.getPointerCount() > 1) {
                final float touchX1 = e.getX(0);
                final float touchY1 = e.getY(0);
                final float touchX2 = e.getX(1);
                final float touchY2 = e.getY(1);
                return (float) (Math.atan2(touchY2 - touchY1, touchX2 - touchX1) * (180f 
                / Math.PI));
            }
            return 0;
        }

複製程式碼


計算兩點距離很簡單,中學的計算公式

兩點之間求距離公式
求兩點相減的平方求根之後就是直線距離了。
求斜率也是藉助中學的計算公式
斜率公式
算出來斜率,不過此時的斜率不能直接計算,要轉換成角度。而轉換成角度,只需要乘以(180÷π)即可。


那麼我們求出角度和距離公式之後,只需要跟上一次記錄的資料進行比對,即可改變資料。我們看看實現效果。

觸控效果


但是到這一步還沒有完,我們還要加上吸附動畫。

動畫

我們先直接看看吸附動畫的程式碼:

 private void runAngleAdsorbentAnim(int pos) {
        // force run animation
        if (pos >= imgList.size() || pos < 0)
            return;
        mViewMode = VIEW_MODE_RUN_ANIMATION;
        final ImageData imageData = imgList.get(pos);
        /*
          吸附運算方式:
          e.g:

          space = 100;
          left point = 100;
          right point = 200;

          x = 161;
          calc process:

          161+50 = 211
          211/100 = 2
          2x100=200

          x = 149
          calc process:

          149+50 = 199
          199/100 = 1
          1x100 = 100

          為了保證運算方式的結果,
          以int形式進行計算,運算
          結果出來之後再轉換為rate
         */
        final int adsorbentAngle = 90;
        final int orgAngle = (int) imageData.rotateAngle;
        int toAngle = ((orgAngle + (adsorbentAngle / 2)) / adsorbentAngle) * adsorbentAngle;
        ValueAnimator valueAnimator = ValueAnimator.ofFloat(orgAngle, toAngle);
        valueAnimator.setDuration(DEFAULT_ANIMATION_TIME);
        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                imageData.rotateAngle = (float) animation.getAnimatedValue();
                reDraw();
            }
        });
        valueAnimator.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                mViewMode = VIEW_MODE_IDLE;
            }
        });
        valueAnimator.start();
    }
複製程式碼

吸附的運算原理在註釋中已經詳細距離說明了。這裡就不再解釋了。傳參進來一個img的座標,使用ValueAnimator對其屬性進行改變,呼叫reDraw()方法即可完成一幀的動畫。
最後再來看看新增完吸附動畫之後的效果:

帶有吸附效果的觸控實現

最後我們到這裡基本的控制操作就完成了,還差最後一步,就是最終的圖片拼接。

圖片拼接

android的View給我們提供了getDrawingCache()方法來獲得當前view的繪製介面,不過這個方法受很多因素影響,不能每次都可以呼叫成功,並且可能會發生不可預知的後續操作,開啟DrawingCache會產生效能影響。所以我們自己建立一個Cavans,傳給onDraw()方法,讓其把當前最新的介面繪製到我們傳給他的Cavans上面。程式碼如下:

   private Thread handleBitmapThread = new Thread(new Runnable() {
        @Override
        public void run() {
            try {
                outputBitmap = Bitmap.createBitmap(getMeasuredWidth(),
                        getMeasuredHeight(), Bitmap.Config.ARGB_8888);
                Canvas canvas = new Canvas(outputBitmap);
                draw(canvas);
                generateBitmapHandler.sendEmptyMessage(BITMAP_GENERATE_RESULT);
            } catch (Exception e) {
                // 扔到主執行緒丟擲
                Message message = new Message();
                message.what = BITMAP_GENERATE_ERROR;
                Bundle bundle = new Bundle();
                bundle.putSerializable(BITMAP_ERROR, e);
                message.setData(bundle);
                generateBitmapHandler.sendMessage(message);
            }
        }
    });
複製程式碼

使用自己建立的Cavans還可以限定畫布大小,達到裁剪的目的。onDraw()方法執行完成之後,介面繪製到了我們傳遞的Bitmap上面,就可以把Bitmap丟擲給處理方法來實現顯示或者儲存等一系列操作。


本文程式碼:github.com/Kongdy/Imag…
個人github地址:github.com/Kongdy
個人掘金主頁:juejin.im/user/595a64…
csdn主頁:blog.csdn.net/u014303003

相關文章