溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點(diǎn)擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

Android如何實(shí)現(xiàn)自定義view之畫圖板

發(fā)布時(shí)間:2021-07-08 14:26:30 來源:億速云 閱讀:160 作者:小新 欄目:移動開發(fā)

這篇文章主要介紹了Android如何實(shí)現(xiàn)自定義view之畫圖板,具有一定借鑒價(jià)值,感興趣的朋友可以參考下,希望大家閱讀完這篇文章之后大有收獲,下面讓小編帶著大家一起了解一下。

看效果: 中間一個(gè)畫圖板 上方小控件用來顯示實(shí)時(shí)畫出的圖形 下方小控件用來做一些畫圖的控制 2個(gè)小控件都能移動

Android如何實(shí)現(xiàn)自定義view之畫圖板

順帶還有一個(gè)刮刮卡效果,只需要改一個(gè)參數(shù):

Android如何實(shí)現(xiàn)自定義view之畫圖板

自定義view首先要自定義屬性:

在values下面創(chuàng)建attrs.xml:

 <!--畫圖板-->
  <declare-styleable name="DrawImg">
    <attr name="PaintColor" />      //畫筆顏色
    <attr name="PaintWidth" />      // 畫筆寬度
    <attr name="CanvasImg" />      //畫板圖片
  </declare-styleable>

  <!--指定單位-->
  <attr name="PaintColor" format="color" />    
  <attr name="PaintWidth" format="dimension" />     
  <attr name="CanvasImg" format="reference" />

對于下面3行指定單位的代碼可以放出來,可以讓多個(gè)自定義view 都能使用。

接下來新建自定義view類繼承view,重寫前3個(gè)構(gòu)造方法

紅線標(biāo)注是android studio 3.0.0對于參數(shù)提示的新特性

Android如何實(shí)現(xiàn)自定義view之畫圖板

通過this 讓前2個(gè)構(gòu)造方法都實(shí)現(xiàn)3個(gè)參數(shù)的構(gòu)造方法。
簡單說一下構(gòu)造方法。一個(gè)參數(shù)的構(gòu)造方法是在代碼中 new 時(shí)用到,2個(gè)參數(shù)的構(gòu)造方法在布局xml中用到,3個(gè)參數(shù)的基本就是自定義view類中使用,大概就是這樣。

接下來從attrs.xml中通過TypedArray取出自定義屬性:

//從attrs文件中取出各個(gè)屬性
    TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.DrawImg, defStyleAttr, 0);
    for (int i = 0; i < a.getIndexCount(); i++) {
      int attr = a.getIndex(i);
      switch (attr) {
        case R.styleable.DrawImg_PaintWidth:    //畫筆寬度
          paintWidth = a.getDimensionPixelSize(attr, (int) TypedValue.applyDimension(
              TypedValue.COMPLEX_UNIT_DIP, -1, getResources().getDisplayMetrics()));
          break;
        case R.styleable.DrawImg_PaintColor:    //畫筆顏色
          paintColor = a.getColor(attr, Color.GREEN);
          break;
        case R.styleable.DrawImg_CanvasImg:     //畫板圖片
          hasCanvasImg = a.getResourceId(attr, -1);
          break;
      }
    }
    //設(shè)置默認(rèn)畫筆寬度
    if (paintWidth == -1) {
      paintWidth = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 20, context.getResources().getDisplayMetrics());
    }
    //取出bitmap
    if (hasCanvasImg != -1) {
      bitmap = BitmapFactory.decodeResource(getResources(), hasCanvasImg);
    }
    //onMeasure可能走多次,onDraw創(chuàng)建對象更不好 所以把畫筆路徑new在這里
    path = new Path();

需要默認(rèn)值的設(shè)置默認(rèn)值,以免布局中沒有用到自定義屬性導(dǎo)致報(bào)錯(cuò)。

重寫自定義view關(guān)鍵方法onMeasure(),onDraw()。onMeasure()用來指定這個(gè)自定義view 的大小,onDraw()用來進(jìn)行實(shí)時(shí)繪圖

最重要的3個(gè)東西:畫布Canvas,畫筆Paint,路徑Path

代碼略長但是注釋很全,把需要注意的提出來

在newPaint()方法中,paint有一個(gè)setXfermode()方法,這個(gè)表示圖形混合方式,有18種 ~(比下圖多了ADD和OVERLAY)~。給張圖看一下。這里我們用到2種 SRC_IN和 DST_OUT。

SRC_IN:取兩層交集部分,顯示上層
DST_OUT:取兩層非交集部分,顯示下層
說實(shí)話這么說也很難懂,還是要自己動手試一試,不過這里只要知道:
使用SRC_IN就會有一個(gè)畫圖板的效果
使用DST_OUT就會有一個(gè)刮刮卡的效果

Android如何實(shí)現(xiàn)自定義view之畫圖板

/**
   * onMeasure常見方法
   * 1) getChildCount():獲取子View的數(shù)量;
   * 2) getChildAt(i):獲取第i個(gè)子控件;
   * 3) subView.getLayoutParams().width/height:設(shè)置或獲取子控件的寬或高;
   * 4) measureChild(child, widthMeasureSpec, heightMeasureSpec):測量子View的寬高;
   * 5) child.getMeasuredHeight/width():執(zhí)行完measureChild()方法后就可以通過這種方式獲取子View的寬高值;
   * 6) getPaddingLeft/Right/Top/Bottom():獲取控件的四周內(nèi)邊距;
   * 7) setMeasuredDimension(width, height):重新設(shè)置控件的寬高。如果寫了這句代碼,就需要?jiǎng)h除
   * “super. onMeasure(widthMeasureSpec, heightMeasureSpec);”這行代碼。
   */
  @Override
  protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    /**
     * getMode獲取測量模式(下面3種) 和 getSize獲取測量值
     *
     * EXACTLY:當(dāng)寬高值設(shè)置為具體值時(shí)使用,如100dp、match_parent等,此時(shí)取出的size是精確的尺寸;
     * AT_MOST:當(dāng)寬高值設(shè)置為wrap_content時(shí)使用,此時(shí)取出的size是控件最大可獲得的空間;
     * UNSPECIFIED:當(dāng)沒有指定寬高值時(shí)使用(很少見)。
     *
     * */
    //測量模式_寬
    int widthMode = MeasureSpec.getMode(widthMeasureSpec);
    //測量模式_高
    int heightMode = MeasureSpec.getMode(heightMeasureSpec);
    //寬度
    int widthSize = MeasureSpec.getSize(widthMeasureSpec);
    //高度
    int heightSize = MeasureSpec.getSize(heightMeasureSpec);

    //設(shè)置view寬度
    //如果布局中給出了準(zhǔn)確的寬度,直接使用寬度,否則設(shè)置圖片寬度為view寬度
    if (widthMode == MeasureSpec.EXACTLY) {
      width = widthSize;
    } else {
      if (hasCanvasImg != -1) {
        //如果設(shè)置了圖片,使用圖片寬
        width = bitmap.getWidth();
      } else {
        //沒有設(shè)置圖片并且也沒給準(zhǔn)確的view寬高 設(shè)置一個(gè)寬默認(rèn)值
        width = 500;
      }
    }
    //設(shè)置view高度同上
    if (heightMode == MeasureSpec.EXACTLY) {
      height = heightSize;
    } else {
      if (hasCanvasImg != -1) {
        height = bitmap.getHeight();
      } else {
        height = 500;
      }
    }
    //重新設(shè)置view的寬高
    setMeasuredDimension(width, height);

    //設(shè)置畫布以及畫筆
    newPaint();
  }
  private void newPaint() {
    //根據(jù)參數(shù)創(chuàng)建一個(gè)新的bitmap 最后一個(gè)參數(shù)為為儲存形式
    newBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
    //保存bitmap中所有像素點(diǎn)的數(shù)組 
    bmPixels = new int[newBitmap.getWidth() * newBitmap.getHeight()];
    //new帶參的Canvas,其中的bitmap參數(shù) 必須通過createBitmap得到;
    //否則會報(bào)錯(cuò):IllegalStateException : Immutable bitmap passed to Canvas constructor
    canvas = new Canvas(newBitmap);
    if (hasCanvasImg == -1) {
      //如果沒有設(shè)置圖片,則默認(rèn)用灰色覆蓋
      canvas.drawColor(Color.GRAY);
    } else {
      //把設(shè)置的圖片縮放到view大小
      bitmap = zoomBitmap(this.bitmap, width, height);
      canvas.drawBitmap(bitmap, 0, 0, null);
    }
    // 準(zhǔn)備繪制刮卡線條的畫筆
    paint = new Paint();
    paint.setColor(paintColor);
    paint.setStyle(Paint.Style.STROKE);
    paint.setStrokeWidth(paintWidth);
    //設(shè)置是否使用抗鋸齒功能,抗鋸齒功能會消耗較大資源,繪制圖形的速度會減慢
    paint.setAntiAlias(true);
    //設(shè)置是否使用圖像抖動處理,會使圖像顏色更加平滑飽滿,更加清晰
    paint.setDither(true);
    //當(dāng)設(shè)置畫筆樣式為STROKE或FILL_OR_STROKE時(shí),設(shè)置筆刷的圖形樣式
    paint.setStrokeCap(Paint.Cap.ROUND);
    //設(shè)置繪制時(shí)各圖形的結(jié)合方式
    paint.setStrokeJoin(Paint.Join.ROUND);
    //設(shè)置圖形重疊時(shí)的處理方式
    /**
     * SRC_IN:取兩層繪制交集。顯示上層
     */
    paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
  }

  //這個(gè)onDraw方法只有一句代碼,意思是在手指移動的同時(shí)把畫板圖片繪制出來
  @Override
  protected void onDraw(Canvas canvas) {
    canvas.drawBitmap(newBitmap, 0, 0, null);
    super.onDraw(canvas);
  }

  //將指定圖片縮放到指定寬高,返回新的圖片Bitmap對象
  public static Bitmap zoomBitmap(Bitmap bm, int newWidth, int newHeight) {
    // 獲得圖片的寬高
    int width = bm.getWidth();
    int height = bm.getHeight();
    // 計(jì)算縮放比例
    float scaleWidth = ((float) newWidth) / width;
    float scaleHeight = ((float) newHeight) / height;
    // 取得想要縮放的matrix參數(shù)
    Matrix matrix = new Matrix();
    matrix.postScale(scaleWidth, scaleHeight);
    // 得到新的圖片
    return Bitmap.createBitmap(bm, 0, 0, width, height, matrix, true);
  }

這是一堆對于這個(gè)view來說比較復(fù)雜的代碼,但是功能很簡單,我們做了2件事:

1.通過MeasureSpec.getMode(測量模式),計(jì)算出整個(gè)控件的寬高
2.通過canvas.drawBitmap在畫布上畫出bitmap,同時(shí) new 出畫筆 Paint 給它設(shè)置顏色,粗細(xì)等屬性

注意:

1.onDraw()方法在每次調(diào)用invalidate(),或者視圖變化時(shí)都會重走,所以不能在里面 new 東西.
2.有一個(gè)int[]類型的數(shù)組 bmPixels,這里大概說一下是個(gè)什么意思,具體的解釋在Bitmap類getPixels和createBitmap方法詳解中有說道。

bmPixels: 我們通過bitmap的寬度乘以高度,可以的到一個(gè)int[]類型的數(shù)組,這個(gè)數(shù)組就是組成bitmap的所有像素點(diǎn),某一個(gè)像素點(diǎn)為0的時(shí)候就說明他是沒有顏色,!0就說明是有顏色的。

既然是畫圖,那肯定要監(jiān)聽手指移動,onTouchEvent()方法:

@Override
  public boolean onTouchEvent(MotionEvent event) {
    int currX = (int) event.getX();
    int currY = (int) event.getY();
    switch (event.getAction()) {
      case MotionEvent.ACTION_DOWN:
        //按下時(shí),設(shè)置線條的起始點(diǎn)準(zhǔn)備繪制
        path.moveTo(currX, currY);
        break;
      case MotionEvent.ACTION_MOVE:
        //滑動時(shí),繪制路徑
        path.lineTo(currX, currY);
        break;
      case MotionEvent.ACTION_UP:
    }
    // 繪制線條,請求重繪整個(gè)控件
    canvas.drawPath(path, paint);
    //請求View樹進(jìn)行重繪,即draw()方法,如果視圖的大小發(fā)生了變化,還會調(diào)用layout()方法。
    invalidate();
    return true;
  }

這個(gè)就很簡單,手指按下時(shí)記錄位置,path.moveTo給path設(shè)置起始點(diǎn)位置,移動時(shí)通過path.lineTo()方法記錄路徑,同時(shí)使用 canvas.drawPath(path, paint)直接繪制出來,invalidate()通知視圖更新。

寫到這里,在xml布局中使用這個(gè)view,已經(jīng)能畫一畫了

我們的畫筆Paint類,可以指定顏色,粗細(xì),模式,等等,這樣我們就可以寫一些公開的方法,給它動態(tài)的設(shè)置這些屬性,從而讓畫筆更加多樣性。

//設(shè)置畫筆顏色
  public void setPaintColor(int color) {
    //path = new Path();
      path.reset();
    paint.setColor(color);
  }

  //設(shè)置畫筆類型
  public void setPaintMode(int style) {
    //path = new Path();
      path.reset();
    /**
     * SRC_IN:取兩層交集部分,顯示上層
     * DST_OUT:取兩層非交集部分,顯示下層
     */
    if (style == 1) {
      paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
    } else {
      paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT));
    }
    resetCanvaas();
  }

  //設(shè)置畫布重置
  public void resetCanvaas() {
    //path = new Path();
      path.reset();
    canvas.drawBitmap(bitmap, 0, 0, null);
    invalidate();
    listener.bitmapChangeListener(bitmap);
  }

上面代碼 設(shè)置畫筆顏色 ,設(shè)置畫筆類型以及畫布重置為什么都要new Path呢,因?yàn)槿绻恍麻_一個(gè)路徑給畫筆,當(dāng)你設(shè)置了新的顏色,用的還是以前的Path,畫筆就會把以前的Path也重新設(shè)置新顏色,而不是保持原來的顏色。

這樣就會出現(xiàn)一個(gè)問題,每次都在new Path,new一次創(chuàng)建一次,占用一次內(nèi)存,想到一些避免方法,但是本文畫圖不是重點(diǎn),就不在論述。(已改用path.reset())

效果中的右上角,顯示了一個(gè)float類型的數(shù),它是在刮刮卡模式下,已經(jīng)抹掉部分所占bitmap的比例,onMeasure()方法中有一個(gè)int[]類型的數(shù)組 bmPixels ,這個(gè)時(shí)候我們就要利用這個(gè)數(shù)組來得到這個(gè)比例。

Android如何實(shí)現(xiàn)自定義view之畫圖板

在onTouchEvent()方法的case MotionEvent.ACTION_UP加上一些代碼:

@Override
  public boolean onTouchEvent(MotionEvent event) {
    int currX = (int) event.getX();
    int currY = (int) event.getY();
    switch (event.getAction()) {
      case MotionEvent.ACTION_DOWN:
        //按下時(shí),設(shè)置線條的起始點(diǎn)準(zhǔn)備繪制
        path.moveTo(currX, currY);
        break;
      case MotionEvent.ACTION_MOVE:
        //滑動時(shí),繪制路徑
        path.lineTo(currX, currY);
        //通過回調(diào),實(shí)時(shí)把bitmap顯示出去
        listener.bitmapChangeListener(newBitmap);
        break;
      case MotionEvent.ACTION_UP:
        //抬起手指時(shí),計(jì)算圖片抹去了多少
        int nullPixel = 0;
        newBitmap.getPixels(bmPixels, 0, width, 0, 0, width, height);
        for (int i = 0; i < bmPixels.length; i++) {
          //抹去部分的像素點(diǎn)在數(shù)組中就會表示為0,找出為0的個(gè)數(shù)
          if (bmPixels[i] == 0) {
            nullPixel++;
          }
        }
        //計(jì)算抹去部分所占的百分比
        listener.showBitmapClear((float) nullPixel / (float) bmPixels.length);
        break;
    }
    // 繪制線條,請求重繪整個(gè)控件
    canvas.drawPath(path, paint);
    //請求View樹進(jìn)行重繪,即draw()方法,如果視圖的大小發(fā)生了變化,還會調(diào)用layout()方法。
    invalidate();
    return true;
  }

有一句 newBitmap.getPixels(bmPixels, 0, width, 0, 0, width, height);在getPixels方法詳解中有解釋,它的作用就是把newBitmap 中所有的像素點(diǎn)全部取出來,放到方法中的第一個(gè)參數(shù)bmPixels中。這個(gè)時(shí)候,我們再通過for循環(huán)遍歷bmPixels數(shù)組,等于0的說明是沒有顏色被抹掉的,統(tǒng)計(jì)他們的數(shù)量,計(jì)算他們所占的比例,就能算出抹掉的比例。同理我們也可以改變等于0這個(gè)判斷條件,讓他等于其他顏色,這樣也就可以計(jì)算其他顏色所占比例。
寫個(gè)回調(diào)接口,在代碼中取出來就OK了。

//回調(diào)接口
  public interface bitmapListener {
    //實(shí)時(shí)的把繪制的bitmap顯示在imageview 上
    void bitmapChangeListener(Bitmap bitmap);
    //顯示抹掉比例
    void showBitmapClear(float clear);
  }

  public void addBitmapListener(bitmapListener bitmapListener) {
    this.listener = bitmapListener;
  }

感謝你能夠認(rèn)真閱讀完這篇文章,希望小編分享的“Android如何實(shí)現(xiàn)自定義view之畫圖板”這篇文章對大家有幫助,同時(shí)也希望大家多多支持億速云,關(guān)注億速云行業(yè)資訊頻道,更多相關(guān)知識等著你來學(xué)習(xí)!

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI