5

When you create a LiveWallpaper in Android 2.2+ you get a canvas (or whatever the 3D equivalent is) to draw on. I'd like to draw some elements using the built-in Android UI tools rather than building everything from scratch using canvas commands or a loading a pre-rendered UI bitmap.

Converting a single View to a Bitmap works fine. i.e. this works great:

// For example this works:
TextView view = new TextView(ctx);
view.layout(0, 0, 200, 100);
view.setText("test");
Bitmap b = Bitmap.createBitmap( 200, 100, Bitmap.Config.ARGB_8888);                
Canvas tempC = new Canvas(b);
view.draw(tempC);
c.drawBitmap(b, 200, 100, mPaint);

But, converting a LinearLayout with children causes problems. You only get the LinearLayout itself and none of it's children. For example, if I set the LinearLayout to have a white background I get a nicely rendered white box, but none of the TextView children are in the Bitmap. I've also tried using DrawingCache with similar results.

The code I'm using is the cube example with the only changes being an extra draw command. The LinearLayout works fine as a toast or as a regular view (i.e. everything nicely shows up), on the LiveWallpaper all I get is the LinearLayout's background rendered.

inflater = (LayoutInflater)getApplicationContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
layout = (LinearLayout) inflater.inflate(com.example.android.livecubes.R.layout.testLinearLayout, null);
layout.layout(0, 0, 400, 200);

Bitmap b = Bitmap.createBitmap( 400, 200, Bitmap.Config.ARGB_8888);
Canvas tempC = new Canvas(b);
layout.draw(tempC);
c.drawBitmap(b, 10, 200, mPaint);

Does anyone know if you need to do anything special to get the children rendered properly to my bitmap? i.e. do I need to somehow do something special to make the layout render the rest of the children? Should I write a function to recursively do something to all the children?

I could composite everything myself but, since the display is fairly static (i.e. I draw this once and keep a copy of the bitmap to draw on the background) this seems easier on me and still pretty efficient.

Edit: While digging a bit more into the state of the Layout it looks as though the layout is not progressing down the view tree (i.e. the LinearLayout gets its layout computed when I call layout() but the children have a null (0x0) size). According to the Romain Guy's post in 2008 android developer post. You have to wait for the layout pass or force the layout yourself. The problem is how can I wait for a layout pass from a wall paper engine for a LinearLayout that is not attached to the root view group? And how can I manually layout each child element if layout requires you to set the left, top, right, bottom when I don't know what these should be.

I've tried calling forceLayout on the children but it doesn't seem to help either. I'm not sure how the layout framework works behind the scenes (besides that it does a two pass layout). Is there a way to manually make it do the layout pass, i.e. right now? Since it's not an Activity I don't think a lot of the normal background stuff is happening quite the way I'd like.

William
  • 171
  • 1
  • 3
  • 7
  • did you implement the proposed solution? I am evaluating the same approach, but would like to learn from your experiences – dparnas Jan 12 '12 at 14:40

2 Answers2

11

Live Wallpapers were very specifically designed to NOT use standard UI widgets. However it is possible to use them anyway. You will have to force a layout pass yourself by first calling measure() on the View, then layout(). You can find more information in my presentation.

MPelletier
  • 16,256
  • 15
  • 86
  • 137
Romain Guy
  • 97,993
  • 18
  • 219
  • 200
  • 14
    @JustinBuser I am one of the engineers who designed and implemented live wallpapers on the Android team. The classes you mention have nothing to do with the standard UI widgets we're talking about. The point is that you cannot add views directly to a live wallpaper (using setContentView(), addView(), etc.) For instance, a ListView. The presentation I linked to is about how to measure and layout Views yourself. – Romain Guy Nov 02 '12 at 00:42
  • 1
    The presentation explains about how to call measure() and layout() on Views. Once a View has been measured and laid out, it can be drawn onto a Canvas, for instance on a live wallpaper. – Romain Guy Nov 16 '12 at 17:57
3

Here's an example of a view group, button and imageview laid out and displayed in a Live Wallpapers. You can also work around the null window token bug and add views directly via WindowManager if you set the the Window type to 0. You have to catch the exception it throws and the results are somewhat erratic but it works for the most part.

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.PixelFormat;
import android.service.wallpaper.WallpaperService;
import android.util.Log;
import android.view.Gravity;
import android.view.SurfaceHolder;
import android.view.SurfaceHolder.Callback;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.view.WindowManager.LayoutParams;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.LinearLayout;

public class UIWidgetWallpaper extends WallpaperService
    {

        private final String    TAG         = getClass().getSimpleName();
        final static int        pixFormat   = PixelFormat.RGBA_8888;
        protected ImageView     imageView;
        protected WindowManager windowManager;
        protected LayoutParams  layoutParams;
        protected WidgetGroup   widgetGroup;
        protected SurfaceHolder surfaceHolder;
        protected Button        button;

        @Override
        public Engine onCreateEngine()
            {
                Log.i( TAG, "onCreateEngine" );
                return new UIWidgetWallpaperEngine();
            }

        public class WidgetGroup extends ViewGroup
            {

                private final String    TAG = getClass().getSimpleName();

                public WidgetGroup( Context context )
                    {
                        super( context );
                        Log.i( TAG, "WidgetGroup" );
                        setWillNotDraw( true );
                    }

                @Override
                protected void onLayout( boolean changed, int l, int t, int r, int b )
                    {
                        layout( l, t, r, b );
                    }

            }

        public class UIWidgetWallpaperEngine extends Engine implements Callback
            {

                private final String    TAG = getClass().getSimpleName();

                @Override
                public void onCreate( SurfaceHolder holder )
                    {
                        Log.i( TAG, "onCreate" );
                        super.onCreate( holder );
                        surfaceHolder = holder;
                        surfaceHolder.addCallback( this );
                        imageView = new ImageView( getApplicationContext() );
                        imageView.setClickable( false );
                        imageView.setImageResource( R.drawable.icon );
                        widgetGroup = new WidgetGroup( getApplicationContext() );
                        widgetGroup.setBackgroundDrawable( getWallpaper() );
                        widgetGroup.setLayoutParams( new LinearLayout.LayoutParams( LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT ) );
                        widgetGroup.setAddStatesFromChildren( true );
                        holder.setFormat( pixFormat );
                        LinearLayout.LayoutParams imageParams = new LinearLayout.LayoutParams( LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT );
                        imageParams.weight = 1.0f;
                        imageParams.gravity = Gravity.CENTER;
                        widgetGroup.addView( imageView, imageParams );
                        button = new Button( getApplicationContext() );
                        button.setText( "Test Button" );
                        LinearLayout.LayoutParams buttonParams = new LinearLayout.LayoutParams( LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT );
                        buttonParams.weight = 1.0f;
                        buttonParams.gravity = Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM;
                        widgetGroup.addView( button, buttonParams );
                    }

                @Override
                public void surfaceChanged( SurfaceHolder holder, int format, int width, int height )
                    {
                        Log.i( TAG, "surfaceChanged: " );
                        synchronized( surfaceHolder )
                            {
                                Canvas canvas = surfaceHolder.lockCanvas();
                                widgetGroup.layout( 0, 0, width, height );
                                imageView.layout( 0, 0, width / 2, height );
                                button.layout( width / 2, height - 100, width, height );
                                widgetGroup.draw( canvas );
                                surfaceHolder.unlockCanvasAndPost( canvas );
                            }
                    }

                @Override
                public void surfaceCreated( SurfaceHolder holder )
                    {
                    }

                @Override
                public void surfaceDestroyed( SurfaceHolder holder )
                    {
                    }

            }

    }
Brad Larson
  • 170,088
  • 45
  • 397
  • 571
Justin Buser
  • 2,813
  • 25
  • 32
  • And this is why we can't have nice things... "You have to catch the exception it throws and the results are somewhat erratic but it works for the most part." Really??? – Dan Dec 01 '12 at 21:35
  • 5
    My answer was mostly a response to the one that states "Live Wallpapers were very specifically designed to NOT use standard UI widgets." which is a ridiculous and misleading answer. That's like saying that airplanes were "specifically designed not to be submersible". My point is basically that just because you don't make something water proof doesn't mean that it can't be done. The 15 minutes I spent writing that example code prove that it in fact CAN be done, however I didn't really feel inclined to debug it thoroughly after proving my point, hence the warning about catching an error. – Justin Buser Dec 31 '12 at 21:49
  • this solution actually works! :D the code is a bit messed up and it lack a few code but it did work. I have a question though. I have a subclass extends GLSurfaceView, which has a 3D object being rendered, how could I make both of them shown in which the 3D object is at the background the the ImageView on top? thanks! – ponnex Feb 17 '16 at 13:02