How to display 2 views, with a gradient-fade effect on Android? How to display 2 views, with a gradient-fade effect on Android? android android

How to display 2 views, with a gradient-fade effect on Android?


Unfortunately AFAIK you can't crossfade between a camera preview and a map if both components have to be interactive/live. Like stated before in a previous comment, this is related to the nature of both widgets and the limitations of Android compositing.

Camera preview needs a SurfaceView in order to work properly. From the official docs:

the SurfaceView punches a hole in its window to allow its surface to be displayed. The view hierarchy will take care of correctly compositing with the Surface any siblings of the SurfaceView that would normally appear on top of it. This can be used to place overlays such as buttons on top of the Surface, though note however that it can have an impact on performance since a full alpha-blended composite will be performed each time the Surface changes.

Google Maps v2 use SurfaceView too (look here), so basically you have two SurfaceView instances one on top of the other, and you simply can't apply a gradient mask in order achieve what you want, because you have no control on how each widget draws itself:

  • Camera preview SurfaceView receive camera buffer and render it natively
  • Maps SurfaceView is rendered in another process.

Furthermore, using two instances of SurfaceView together is highly discouraged like stated here:

The way surface view is implemented is that a separate surface is created and Z-ordered behind its containing window, and transparent pixels drawn into the rectangle where the SurfaceView is so you can see the surface behind. We never intended to allow for multiple surface views.

I think the only option you have is to choose only one of them to be live/interactive and draw the other as a static image gradient on top of it.


EDIT

In order to validate further my previous statements, below a quote from official docs about Camera usage:

Important: Pass a fully initialized SurfaceHolder to setPreviewDisplay(SurfaceHolder). Without a surface, the camera will be unable to start the preview.

So you are forced to use a SurfaceView in order to get the preview from Camera. Always.
And just to repeat myself: you have no control on how those pixels are rendered, because Camera writes directly the framebuffer using the preview SurfaceHolder.

In conclusion you have two fully-opaque SurfaceView instances and you simply can't apply any fancy rendering to their content, so I think such effect is simply impractical in Android.


This possible, but perhaps a bit complicated. To keep it simple, I've put the core code for achieving this in the answer. As has been noted, you need two views to do this, one "on top of" the other. The "lower" one should be a SurfaceView, driven by the maps API. The "higher" one should show the camera image faded out over it.

EDIT: As mr_archano points out, the API is (now) defined such that without a SurfaceView the camera wont send out preview data. Ho hum, such is the nature of progress, However, this is also surmountable.

The code presents:

  • The "lower" SurfaceView is driven directly by the camera preview mechanism.
  • The "middle" SurfaceView is for the MAPS API.
  • The "upper" View is where the camera data is rendered to achieve the desired effect.

The core code therefore gives "camera preview" over "camera preview", and the upper picture has been intentionally distorted so it's clearly visible fully at the top, fading in the middle and gone at the bottom.

May I suggest that the best way to use this code is to implement these first four steps on their own and see it working, then add the two final steps and see that working, before then inserting the key concepts into another, doubtless larger and more complex, piece of code.

First Four steps:

  1. Create a custom view for display to top, camera, view. This class renders a bitmap over whatever is underneath it. The alpha value in each pixel in the bitmap will determine how much of the lower view is let through.

    public class CameraOverlayView extends View {    private Paint  paint;    private Size   incomingSize;    private Bitmap bitmap = null;    public CameraOverlayView(Context context) {        super(context);        init();    }    public CameraOverlayView(Context context, AttributeSet attrs) {        super(context, attrs);        init();    }    private void init() {        paint = new Paint();        paint.setStyle(Style.FILL_AND_STROKE);        paint.setColor(0xffffffff);        paint.setTextSize((float) 20.0);    }    @Override    protected void onDraw(Canvas canvas) {        super.onDraw(canvas);        int width  = canvas.getWidth();        int height = canvas.getHeight();        canvas.drawBitmap(bitmap, 0.0f, 0.0f, paint);    }}
  2. Put three views in a frame with them all set tofill_parent in both directions. The first one will be "underneath" (the SurfaceView so the the camera preview works). The second one "in the middle" (the surface view for Maps or whatever). The third "on top" (the view for the faded camera image).

    <SurfaceView    android:id="@+id/beneathSurfaceView"    android:layout_width="fill_parent"    android:layout_height="fill_parent" /><SurfaceView    android:id="@+id/middleSurfaceView"    android:layout_width="fill_parent"    android:layout_height="fill_parent" /><com.blah.blah.blah.CameraOverlayView    android:id="@+id/aboveCameraView"    android:layout_width="fill_parent"    android:layout_height="fill_parent" />

  3. A stripped down main Activity which will set up the camera, and send the automatic preview image to the (bottom) SurfaceView and the preview image data to a processing routine. It sets a callback to catch the preview data. These two run in parallel.

    public class CameraOverlay extends Activity implements SurfaceHolder.Callback2 {    private SurfaceView       backSV;    private CameraOverlayView cameraV;    private SurfaceHolder cameraH;    private Camera        camera=null;    private Camera.PreviewCallback cameraCPCB;    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.camera_overlay);        // Get the two views                backSV  = (SurfaceView) findViewById(R.id.beneathSurfaceView);        cameraV = (CameraOverlayView) findViewById(R.id.aboveCameraView);        // BACK: Putting the camera on the back SV (replace with whatever is driving that SV)        cameraH  = backSV.getHolder();        cameraH.addCallback(this);        // FRONT: For getting the data from the camera (for the front view)        cameraCPCB = new Camera.PreviewCallback () {            @Override            public void onPreviewFrame(byte[] data, Camera camera) {                cameraV.acceptCameraData(data, camera);            }        };    }    // Making the camera run and stop with state changes    @Override    public void onResume() {        super.onResume();        camera = Camera.open();        camera.startPreview();    }    @Override    public void onPause() {        super.onPause();        camera.setPreviewCallback(null);        camera.stopPreview();        camera.release();        camera=null;    }    private void cameraImageToViewOn() {        // FRONT        cameraV.setIncomingSize(camera.getParameters().getPreviewSize());        camera.setPreviewCallback(cameraCPCB);    }    private void cameraImageToViewOff() {        // FRONT        camera.setPreviewCallback(null);    }    // The callbacks which mean that the Camera does stuff ...    @Override    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {        // If your preview can change or rotate, take care of those events here.        // Make sure to stop the preview before resizing or reformatting it.        if (holder == null) return;        // stop preview before making changes        try {            cameraImageToViewOff(); // FRONT            camera.stopPreview();            } catch (Exception e){            // ignore: tried to stop a non-existent preview        }        // set preview size and make any resize, rotate or reformatting changes here        // start preview with new settings        try {            camera.setPreviewDisplay(holder); //BACK            camera.startPreview();            cameraImageToViewOn(); // FRONT        } catch (Exception e){        }    }    @Override    public void surfaceCreated(SurfaceHolder holder) {        try {            camera.setPreviewDisplay(holder); //BACK            camera.startPreview();            cameraImageToViewOn(); // FRONT        } catch (IOException e) {        }           }    @Override    public void surfaceDestroyed(SurfaceHolder holder) {}    @Override    public void surfaceRedrawNeeded(SurfaceHolder holder) {    }}

    Some things are missing:

    • Ensuring that the camera image is the right orientation
    • Ensuring that the camera preview image is the optimal size

  4. Now, add two functions to the View created in step one. The first ensures that the View knows the size of the incoming image data. The second receives the preview image data, turns it into a bitmap, distorting it along the way both for visibility and to demonstrate the alpha fade.

    public void setIncomingSize(Size size) {    incomingSize = size;    if (bitmap != null) bitmap.recycle();    bitmap = Bitmap.createBitmap(size.width, size.height, Bitmap.Config.ARGB_8888);}public void acceptCameraData(byte[] data, Camera camera) {    int width  = incomingSize.width;    int height = incomingSize.height;    // the bitmap we want to fill with the image    int numPixels = width*height;    // the buffer we fill up which we then fill the bitmap with    IntBuffer intBuffer = IntBuffer.allocate(width*height);    // If you're reusing a buffer, next line imperative to refill from the start, - if not good practice    intBuffer.position(0);    // Get each pixel, one at a time    int Y;    int xby2, yby2;    int R, G, B, alpha;    float U, V, Yf;    for (int y = 0; y < height; y++) {        // Set the transparency based on how far down the image we are:        if (y<200) alpha = 255;          // This image only at the top        else if (y<455) alpha = 455-y;   // Fade over the next 255 lines        else alpha = 0;                  // nothing after that        // For speed's sake, you should probably break out of this loop once alpha is zero ...        for (int x = 0; x < width; x++) {            // Get the Y value, stored in the first block of data            // The logical "AND 0xff" is needed to deal with the signed issue            Y = data[y*width + x] & 0xff;            // Get U and V values, stored after Y values, one per 2x2 block            // of pixels, interleaved. Prepare them as floats with correct range            // ready for calculation later.            xby2 = x/2;            yby2 = y/2;            U = (float)(data[numPixels + 2*xby2 + yby2*width] & 0xff) - 128.0f;            V = (float)(data[numPixels + 2*xby2 + 1 + yby2*width] & 0xff) - 128.0f;            // Do the YUV -> RGB conversion            Yf = 1.164f*((float)Y) - 16.0f;            R = (int)(Yf + 1.596f*V);            G = 2*(int)(Yf - 0.813f*V - 0.391f*U); // Distorted to show effect            B = (int)(Yf + 2.018f*U);            // Clip rgb values to 0-255            R = R < 0 ? 0 : R > 255 ? 255 : R;            G = G < 0 ? 0 : G > 255 ? 255 : G;            B = B < 0 ? 0 : B > 255 ? 255 : B;            // Put that pixel in the buffer            intBuffer.put(Color.argb(alpha, R, G, B));        }    }    // Get buffer ready to be read    intBuffer.flip();    // Push the pixel information from the buffer onto the bitmap.    bitmap.copyPixelsFromBuffer(intBuffer);    this.invalidate();}

    Notes on the second routine:

That code shows the basic idea. To then step to the next phase:

  1. Set the camera Surface view to be sufficiently small to hide behind the non-faded section of the top View. ie, change android:layout_height for it to, say, 60dp.

  2. Set the middle SurfaceView to receive the map information.