Search code examples
androidimageviewandroid-canvas

Android custom image view shape


I am working on creating a custom ImageView which will crop my image into a hexagon shape and add a border. I was wondering if my approach is correct or if I am doing this the wrong way. There are a bunch of custom libraries out there that already do this but none of them out of the box have the shape I am looking for. That being said, this is more a question about the best practice.

expected result

You can see the full class in this gist, but the main question is that is this the best approach. It feels wrong to me, partly because of some of the magic numbers which means it could be messed up on some devices.

Here is the meat of the code:

      @Override
      protected void onDraw(Canvas canvas) {
        Drawable drawable = getDrawable();
        if (drawable == null || getWidth() == 0 || getHeight() == 0) {
          return;
        }

        Bitmap b = ((BitmapDrawable) drawable).getBitmap();
        Bitmap bitmap = b.copy(Bitmap.Config.ARGB_8888, true);

        int dimensionPixelSize = getResources().getDimensionPixelSize(R.dimen.width); // (width and height of ImageView)
        Bitmap drawnBitmap = drawCanvas(bitmap, dimensionPixelSize);
        canvas.drawBitmap(drawnBitmap, 0, 0, null);
      }

      private Bitmap drawCanvas(Bitmap recycledBitmap, int width) {
        final Bitmap bitmap = verifyRecycledBitmap(recycledBitmap, width);

        final Bitmap output = Bitmap.createBitmap(width, width, Bitmap.Config.ARGB_8888);
        final Canvas canvas = new Canvas(output);

        final Rect rect = new Rect(0, 0, width, width);
        final int offset = (int) (width / (double) 2 * Math.tan(30 * Math.PI / (double) 180)); // (width / 2) * tan(30deg)
        final int length = width - (2 * offset);

        final Path path = new Path();
        path.moveTo(width / 2, 0); // top
        path.lineTo(0, offset); // left top
        path.lineTo(0, offset + length); // left bottom
        path.lineTo(width / 2, width); // bottom
        path.lineTo(width, offset + length); // right bottom
        path.lineTo(width, offset); // right top
        path.close(); //back to top

        Paint paint = new Paint();
        paint.setStrokeWidth(4);
        canvas.drawARGB(0, 0, 0, 0);
        canvas.drawPath(path, paint);
        paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
        canvas.drawBitmap(bitmap, rect, rect, paint); // draws the bitmap for the image

        paint.setColor(Color.parseColor("white"));
        paint.setStrokeWidth(4);
        paint.setDither(true);
        paint.setStyle(Paint.Style.STROKE);
        paint.setStrokeJoin(Paint.Join.ROUND);
        paint.setStrokeCap(Paint.Cap.ROUND);
        paint.setPathEffect(new CornerPathEffect(10));
        paint.setAntiAlias(true); // draws the border

        canvas.drawPath(path, paint);

        return output;
      }

I was looking at some iOS code and they are able to apply an actual image as a mask to achieve this result. Is there anyway on Android to do something like that?


Solution

  • I was looking for the best approach for a long time. Your solution is pretty heavy and doesn't work well with animations. The clipPath approach doesn't use antialiasing and doesn't work with hardware acceleration on certain versions of Android (4.0 and 4.1?). Seems like the best approach (animation friendly, antialiased, pretty clean and hardware accelerated) is to use Canvas layers:

    Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
    private static PorterDuffXfermode pdMode = new PorterDuffXfermode(PorterDuff.Mode.CLEAR);
    
    @Override
    public void draw(Canvas canvas) {
            int saveCount = canvas.saveLayer(0, 0, getWidth(), getHeight(),
                                             null, Canvas.ALL_SAVE_FLAG);
    
            super.draw(canvas);
    
            paint.setXfermode(pdMode);
            canvas.drawBitmap(maskBitmap, 0, 0, paint);
    
            canvas.restoreToCount(saveCount);
            paint.setXfermode(null);
    }
    

    You can use any kind of mask including custom shapes and bitmaps. Carbon uses such approach to round corners of widgets on the fly.