Android custom image view shape

6.1k views Asked by At

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?

4

There are 4 answers

2
Zielony On BEST ANSWER

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.

3
Budius On

Even though it might work, there a few bad mistakes on this implementation:

  • You're allocation some very big objects during onDraw phase and that leads to a terrible performance. The most important there is the createBitmap but you should avoid at all costs any new during onDraw. Pre-allocate all necessary objects during initialisation and re-use them during onDraw.

  • You should setup your path just once during onSizeChanged. Avoid all that path on every OnDraw

  • You're relying on usage of BitmapDrawable if for example you use Picasso to load images from the internet or if you want to use a selector, then this code won't work.

  • You should not need to allocate the second bitmap, use canvas.clipPath instead to make it efficient.

Said all that a much more efficient pseudo code for the drawing should be:

@Override
protected void onDraw(Canvas canvas) {
   canvas.save(CLIP_SAVE_FLAG); // save the clipping
   canvas.clipPath(path, Region.Op./*have to test which one*/ ); // cut the canvas
   super.onDraw(canvas); // do the normal drawing
   canvas.restore(); // restore the saved clipping
   canvas.drawPath(path, paint); // draw the extra border
}
0
Tanveer Hasan On

add dependency in build.gradle

   implementation 'com.github.siyamed:android-shape-imageview:0.9.+@aar'
   implementation group: 'net.sf.kxml', name: 'kxml2-min', version: '2.3.0'

xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center_horizontal"
android:orientation="vertical"
tools:context=".MainActivity">

<com.github.siyamed.shapeimageview.HexagonImageView
    android:id="@+id/imageView"
    android:layout_marginTop="100dp"
    android:layout_width="250dp"
    android:layout_height="350dp" />
    </LinearLayout>

activity

public class MainActivity extends AppCompatActivity {
HexagonImageView imageView;
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    imageView=findViewById(R.id.imageView);
    imageView.setImageResource(R.drawable.img);
}}

output

enter image description here

0
Tanveer Hasan On

add dependency in build.gradle

implementation 'com.github.siyamed:android-shape-imageview:0.9.+@aar'

xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center_horizontal"
    android:orientation="vertical"
    tools:context=".MainActivity">
    
    <com.github.siyamed.shapeimageview.mask.PorterShapeImageView
        android:id="@+id/imageView"
        android:layout_width="250dp"
        android:layout_height="350dp"
        app:siShape="@drawable/shape" />
    
</LinearLayout>

activity

public class MainActivity extends AppCompatActivity {

    ImageView imageView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        imageView=findViewById(R.id.imageView);
        imageView.setImageResource(R.drawable.img);
    }
}

drawable resource

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle">
    <solid android:color="#FFC107" />
    <padding android:left="7dp"
        android:top="7dp"
        android:right="7dp"
        android:bottom="7dp" />
    <corners
        android:topLeftRadius="0dip"
        android:topRightRadius="70dip"
        android:bottomLeftRadius="70dip"
        android:bottomRightRadius="0dip" />
</shape>

output

output