ImageWindow. An image view like Google Messenger.


You might wonder how that cool effect on the attachments of Google Messenger app is made. This post will guide through all the steps to get a cool looking ImageView just like that.

Well let's break it through. This is a view hierarchy dump of that app.


As you can see this is a compound view wrapped in one `FrameLayout`. It contains one `ImageButton`, which is the close button you see in the left upper corner, and another `FrameLayout` containing the actual `ImageView` you're attaching.

So far nothing too complicated. Let's get started:


public class ImageWindow extends RelativeLayout {
    private final int CLOSE_SIZE = 32; //google messenger approx 27
    private ImageView mImageView;

    public ImageWindow(Context context) {
        super(context);
        init();
    }

    public ImageWindow(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public ImageWindow(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        init();
    }

    private void init() {
        float density = getResources().getDisplayMetrics().density;
        ImageView closeButton = new ImageView(getContext());
        closeButton.setLayoutParams(new RelativeLayout.LayoutParams((int) (CLOSE_SIZE * density), (int) (CLOSE_SIZE * density)));
        closeButton.setBackgroundResource(R.drawable.close_button_drawable);
        closeButton.setImageResource(R.drawable.ic_action_close);
        mImageView = new CustomImageView(getContext(), CLOSE_SIZE);
        RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
        int margin = (int) (10 * density);
        params.setMargins(margin, margin, 0, 0);
        mImageView.setLayoutParams(params);
        mImageView.setAdjustViewBounds(true);
        addView(mImageView);
        addView(closeButton);
    }
}

So we start our `init()` function by saving the current screen density so we don't have to make constant calls to our resources. Then we create the close button which is just a simple `ImageView`. The current layout width and height of the close button are the `CLOSE_SIZE` we set as a field. We will use 32dp. Messenger uses approx. 27dp but we want a bigger touch area. Next we set the resources for both background and image. The background resource is a simple selector drawable with two colors for pressed and not pressed states. Next we create the `ImageView` we want to display. Notice the `CustomImageView`, but for now let's assume it's a normal `ImageView`. Next we set the layout parameters for our image and a top and left margin (10dp should be good). Finally we add the views to our `RelativeLayout`. First our image and next our close button which will stay on top of the image. Finally we create a method that returns our `ImageView` so we can set it outside of this scope by doing `myImageWindow().getImageView()`.

close_button_drawable.xml

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:state_pressed="true">
        <shape android:shape="oval">
            <solid android:color="#ffa25437"/>
        </shape>

    </item>
    <item>
        <shape android:shape="oval">
            <solid android:color="#ffff7b57"/>
        </shape>
    </item>
</selector>

Now we need to add those round corners we see on the image. Let's see how Messenger is doing that by doing some reverse engineering. This is the `onDraw` of the attachment's `ImageView`.


protected void onDraw(Canvas paramCanvas)
  {
    if (this.zS > 0)
    {
      int i = getWidth();
      int j = getHeight();
      if ((this.zU != i) || (this.zV != j))
      {
        RectF localRectF = new RectF(0.0F, 0.0F, i, j);
        this.zT.reset();
        this.zT.addRoundRect(localRectF, this.zS, this.zS, Path.Direction.CW);
        this.zU = i;
        this.zV = j;
      }
      int k = paramCanvas.getSaveCount();
      paramCanvas.save();
      paramCanvas.clipPath(this.zT);
      super.onDraw(paramCanvas);
      paramCanvas.restoreToCount(k);
      return;
    }
    super.onDraw(paramCanvas);
  }

Hum....Messenger is using a `clipPath` method. And the path is a round rectangle. But this doesn't perform anti alias which means that we can have round corners that are not that round:

Another thing to notice is that there is no `clipPath` with the circle that makes the close border on the left upper corner. What does this mean? Let's check what is the drawable of that `ImageView`:

Hum this is awkward. So Messenger is using an image with a gray border? This means that if the background of the app needs to be changed they'll have to create a new image. We don't want that in our application. So, instead of clipping the path and using a close image with a border, we'll do better. Let's start by creating our `CustomImageView`. Since the `ImageView` needs to know the size of the close button so it can draw a circle with the correct size we will pass our `CLOSE_SIZE` to our constructor.
We will also create our fields. We'll need a `Paint` so we can erase our corners and the close button border, a rectangle path and rectangle the size of our image. Our rectangle will have round corners so we might want to set this corners as a field too and 7dp should do the job.


class CustomImageView extends ImageView {

        private float mCloseSize;
        private Paint mEraser;
        private RectF mRectangle;
        private Path mRectanglePath;
        private int RECTANGLE_CORNER = 7;
        private float mDensity;

        public CustomImageView(Context context, float closeSize) {
            super(context);
            mCloseSize = closeSize;
            init();
        }

        ...
 }

Next on our `init()` method we allocate our fields:


private void init() {
            mEraser = new Paint();
            mEraser.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
            mEraser.setAntiAlias(true);
            mRectanglePath = new Path();
            mDensity = getResources().getDisplayMetrics().density;
            RECTANGLE_CORNER = (int) (RECTANGLE_CORNER * mDensity);
 }

Since our `Paint` will erase content we need it's `Xferode` to be `PorterDuff.Mode.CLEAR`. Enable `AntiAlias` so we don't run into those ugly corners.

Now, as you see, our rectangle is still empty. But since we don't know the size of the image on `onCreate` and we don't want to set it on `onDraw` either we'll use `onSizeChanged` for that:


protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    if (w != oldw || h != oldh) {
        mRectanglePath.reset();
        mRectangle = new RectF(0, 0, getWidth(), getHeight());
        mRectanglePath.addRoundRect(mRectangle, RECTANGLE_CORNER, RECTANGLE_CORNER, Path.Direction.CW);
        mRectanglePath.setFillType(Path.FillType.INVERSE_EVEN_ODD);
    }
    super.onSizeChanged(w, h, oldw, oldh);
}

So here we set the rectangle and our path only when the view changes size. We set it's fill type to `INVERSE_EVEN_ODD` because we don't want to erase the content of the rounded rectangle, we want to erase the inverse. Next let's code our `onDraw`.


@Override
protected void onDraw(Canvas canvas) {
    canvas.saveLayerAlpha(0, 0, canvas.getWidth(), canvas.getHeight(), 255, Canvas.HAS_ALPHA_LAYER_SAVE_FLAG);
    super.onDraw(canvas);
    canvas.drawPath(mRectanglePath, mEraser);
    //3 is the margin of the close circle
    canvas.drawCircle((int) ((mCloseSize * 0.5) * mDensity - ((LayoutParams) getLayoutParams()).leftMargin),
            (int) ((mCloseSize * 0.5) * mDensity - ((LayoutParams) getLayoutParams()).topMargin),
            (int) (((mCloseSize * 0.5) + 3) * mDensity), mEraser);
}

Let me explain this logic. First we call `saveLayerAlpha`. This will allocate an offset bitmap with alpha. Why we do this? Because our view may be opaque and our image may not have an alpha. Since we will erase content we don't want to run into a black background. Next we call `super.onDraw` so our actual image gets drawn on the view. Next we erase our rectangle path for round corners and finally we erase a circle the size of the close button plus 3dp. 3dp should be enough but you can always change this value for a bigger border.

The result is this awesome view:



This concludes my explanation. What can you do next to improve this? Well, you can start by adding a callback to your close button so you can know when the user clicked it for example. I'll leave it up to you to change it to fulfill your needs.



The full code is available on my github,

Unknown

Related Posts

4 comments

  1. Hiii...
    I have read this blog...very informative and applicable to this...Thank for sharing..
    Android Training

    ReplyDelete
  2. Hey,
    A great post! Thanks for sharing. I have tried this and actually it is working perfect. Love this Image View.

    ReplyDelete
  3. What a complete posting tutorials, thanks it helps so much, and of course your upcoming tutorials are waited admin.

    ReplyDelete