11

I want to have 2 views displayed on the screen - one would be a camera preview, on the top, while other one would show an image or a google map - and live on the bottom of the screen.

I want to have a gradient-like transition between them though - so there's no rough edge between them. Is that possible to have such an effect?

Edit: The effect I'd like to achieve should look like this (top part comes from the camera preview, while bottom part should be a map...):

Map blending into camera photo

On the iOS I got similar effect with CameraOverlay showing the map and setting the layer masp to the gradient:

CAGradientLayer *gradient = [CAGradientLayer layer];
gradient.frame = self.map.bounds;
gradient.colors = [NSArray arrayWithObjects:(id)[[UIColor colorWithWhite: 1.0 alpha: 0.0] CGColor], (id)[[UIColor colorWithWhite: 1.0 alpha: 1.0] CGColor], nil];
gradient.startPoint = CGPointMake(0.5f, 0.0f);
gradient.endPoint = CGPointMake(0.5f, 0.5f);
self.map.layer.mask = gradient;
kender
  • 85,663
  • 26
  • 103
  • 145

2 Answers2

4

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.

a.bertucci
  • 12,142
  • 2
  • 31
  • 32
  • Hi mr_archano, you are obviously correct, if you stick to using SurfaceViews. But there is another way of rendering camera preview images on the screen, in which you catch the preview data and render it yourself ... – Neil Townsend Apr 23 '13 at 21:47
  • Yes, you can render camera bits in another view, but you cannot get rid of the surfaceview bound to the camera. That's my point: you can't render only the faded frame and hide/remove the surfaceview used to start the preview. – a.bertucci Apr 24 '13 at 13:19
  • Interesting, the API has changed since I used it. However, the solution is relatively straightforwards: you set the SurfaceView whcih the camera uses to be behind another view, make it small and hidden or so on. I'll correct my answer accordingly. – Neil Townsend Apr 24 '13 at 14:51
  • I don't think the API changed AFAICT. When I used that in feb 2011 for this demo (https://play.google.com/store/apps/details?id=com.ssd.app.demo.nfd) it was already this way. – a.bertucci Apr 24 '13 at 15:01
  • 1
    Well, I wrote a piece of code a couple of years ago that worked absolutely fine without using a holder, I re ran the code today and it needed a holder - ho hum. However, the solution, as I've added, is to hide the Camera's SurfaceView somewhere and use that ... – Neil Townsend Apr 24 '13 at 15:06
0

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.

Community
  • 1
  • 1
Neil Townsend
  • 6,024
  • 5
  • 35
  • 52
  • And how do I modify the camera output? So far I used the code similar to http://stackoverflow.com/questions/2456802/android-camera-preview, putting the camera output on the SurfaceView – kender Apr 21 '13 at 16:38
  • Are you using SurfaceView or ImageView? Or one of each? If one of each, which is which? – Neil Townsend Apr 22 '13 at 10:53
  • Right now I'm using 2 SurfaceViews. I guess I could ask camera for a bitmap like every 0.1second and put it on the ImageView, but not sure if this makes sense... – kender Apr 22 '13 at 11:05
  • You will need to do the preview image to screen yourself, as I've summarised in an updated answer. If you think that this is a potentially acceptable route, I'll put some more details in. – Neil Townsend Apr 22 '13 at 16:29
  • Yes, this sounds like the way to go (the only acceptable way, apparently :) – kender Apr 23 '13 at 09:48
  • It's up now! Let me know of any issues – Neil Townsend Apr 23 '13 at 17:00
  • Sorry @NeilTownsend, but I can't get it: the code posted above just show a surfaceview( used for camera preview ) with a custom view on top of it, which *should* draw a preview frame. **A preview on top of a preview**. No gradient transition. No GMapv2 interactive component beneath. Maybe I miss something? – a.bertucci Apr 23 '13 at 17:01
  • So **NOW** (6th edit of your answer so far http://stackoverflow.com/posts/16132202/revisions) you just added a simple way to get the gradient effect. Clever. But you claim that a different `SurfaceHolder` can be used in order to get the bits from camera preview, even the one from `MapsView`. Have you tried? :) – a.bertucci Apr 23 '13 at 22:38
  • The way to get the gradient was in the previous version, it was all in one line and not commented - I hadn't made it clear enough, so I made it clearer following your comment. I have tried and run the code in the answer (hence the long gap before the edit which put all the code in) and it works as I say it will: the base SurfaceView shows the direct camera preview (but should be drivable by anything that drives a surface view) and the overlaid view shows a distorted and faded out version of the camera picture laid over the original. It's quite a fun effect! – Neil Townsend Apr 24 '13 at 09:37