Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Cannot Display 2 Instances of my custom SurfaceView

I've created my own custom SurfaceView which works fine on its own but when I try putting two onto separate tabs in a TabWidget only one is ever shown regardless of which tab is selected, and it is always the SurfaceView which is first drawn when the app starts.

To illustrate the problem I have created sample code which can be compiled to display to problem.

The SurfaceView below, called SurfaceViewCircle simply creates a bitmap, draws a blue circle as default and then displays it. There is a public method, changeColour(), which will change the circle colour in the bitmap.

Secondly, I create an XML layout which simply contains a single instance of SurfaceViewCircle.

In the Activity class, I create a TabWidget and host etc. I then inflate the above XML twice but in one instance I change the colour of the SurfaceViewCircle to red. Once the application runs, no matter what tab I select, the red circle is ALWAYS shown EXCEPT for a brief instance when the app exits and the blue circle is displayed.

Can anyone point out if I have missed out a step when using SurfaceView?

This is the Activity code:

public class TestActivity extends Activity  {
/** Called when the activity is first created. */

private TabHost mTabHost;
private Context mTabHostContext;
private View surfaceView1, surfaceView2;
@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    /*
     * Setup tabs
     */
    setContentView(R.layout.maintabs);
        setupTabHost(); //Prepares the TabHost from code rather than XML;
    mTabHost.getTabWidget().setDividerDrawable(R.drawable.tab_divider); //Sets a thin dividing line
    mTabHostContext = mTabHost.getContext();
    surfaceView1 = LayoutInflater.from(mTabHostContext).inflate(R.layout.surfaceviewindependent, null);
    SurfaceViewCircle s = (SurfaceViewCircle)surfaceView1.findViewById(R.id.circle1);
    /*
     * Change the colour to red
     */
    s.changeColour(getResources().getColor(R.color.red_square));

    /*
     * Create a second layout containing SurfaceViewCircle but leave circle as default blue.
     */
    surfaceView2 = LayoutInflater.from(mTabHostContext).inflate(R.layout.surfaceviewindependent, null);
    setupTab(surfaceView1,"SurfaceView1");
    setupTab(surfaceView2,"SurfaceView2");


}

private void setupTabHost() {
    mTabHost = (TabHost) findViewById(android.R.id.tabhost);
    mTabHost.setup();
}

private void setupTab(final View view, final String tag) {
    View tabview = createTabView(mTabHost.getContext(), tag); // This creates a view to be used in the TAB only

    /* this creates the tab content AND applies the TAB created in the previous step in one go */
    TabSpec setContent = mTabHost.newTabSpec(tag).setIndicator(tabview).setContent(new TabContentFactory() {
        public View createTabContent(String tag) {return view;}
    });
    mTabHost.addTab(setContent);

}

private static View createTabView(final Context context, final String text) {
    View view = LayoutInflater.from(context).inflate(R.layout.tabs_bg, null);
    TextView tv = (TextView) view.findViewById(R.id.tabsText);
    tv.setText(text);

    return view;
}   
}

This is my custom SurfaceView:

public class SurfaceViewCircle extends SurfaceView implements SurfaceHolder.Callback{

private Paint paint, circlePaint;
private Bitmap bitmap = null;
private int w;
private int h;
private int colour = 0;
private Resources r = null;
private _Thread t = null;
private boolean surfaceIsCreated;

public SurfaceViewCircle(Context context) {
    super(context);
    initialise();
}

public SurfaceViewCircle(Context context, AttributeSet attrs) {
    super(context, attrs);
    initialise();
}

public SurfaceViewCircle(Context context, AttributeSet attrs, int defStyle) {
    super(context, attrs, defStyle);
    initialise();
}

private void initialise(){
    r = getResources();
    getHolder().addCallback(this);
    paint = new Paint(Paint.ANTI_ALIAS_FLAG);
    paint.setFilterBitmap(true);
    colour = R.color.blue_square;
    circlePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    circlePaint.setColor(r.getColor(colour));
    circlePaint.setStyle(Style.FILL);
    circlePaint.setStrokeWidth(0.02f);
    t = new _Thread(getHolder());


}

public void changeColour(int colour){
    circlePaint.setColor(colour);
    if (surfaceIsCreated){
        createBitmap();
    }
    synchronized (t){
        t.notify();
    }
}

private Bitmap createBitmap(){
    Bitmap b = null;
    b = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
    Canvas c = new Canvas(b);
    c.scale((float)w, (float)w);        //Scales the background for whatever pixel size
    c.drawCircle(0.5f, 0.5f, 0.5f, circlePaint);
    //c.drawColor(r.getColor(colour));
    return b;
}

public void onMeasure(int widthMeasureSpec, int heightMeasureSpec){
    int width = measure(widthMeasureSpec);
    int height = measure(heightMeasureSpec);

    int d = Math.min(width, height);
    setMeasuredDimension(d,d);
}

private int measure(int measureSpec) {
    int result = 0;
    // Decode the measurement specifications
    int specMode = MeasureSpec.getMode(measureSpec);
    int specSize = MeasureSpec.getSize(measureSpec);

    return specSize;
}

@Override
protected void onSizeChanged(int w, int h, int oldW, int oldH){
    super.onSizeChanged(w, h, oldW, oldH);
    //synchronized (this){
        this.w = Math.min(w, h);
        this.h = w;
    //}
    Bitmap b = createBitmap();

        bitmap = b;

    Log.i("Square", "onSizeChanged() called.");


}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width,
        int height) {
    // TODO Auto-generated method stub

}

@Override
public void surfaceCreated(SurfaceHolder holder) {
    Log.i("Panel", "surfaceCreated() called.");
    t.setRunning(true);
    t.start();
    surfaceIsCreated = true;

}

@Override
public void surfaceDestroyed(SurfaceHolder holder) {
    Log.i("Square", "surfaceDestroyed() called.");

    surfaceIsCreated = false;
    boolean retry = true;
    synchronized (t){
        t.setRunning(false);
        t.notify();
    }
    while (retry) {
        try {
            t.join();
            retry = false;
        } catch (InterruptedException e) {
            // we will try it again and again...
        }
    }

}

private class _Thread extends Thread {
    private SurfaceHolder _surfaceHolder;
    private boolean _run = false;

    public _Thread(SurfaceHolder surfaceHolder) {
        _surfaceHolder = surfaceHolder;
    }

    public void setRunning(boolean run) {
        _run = run;
    }

    @Override
    public void run() {
        Canvas c = null;
        while (_run){
            try {
                c = _surfaceHolder.lockCanvas(null);
                synchronized (_surfaceHolder) {
                    synchronized(bitmap){
                        c.drawBitmap(bitmap, 0, 0, paint);
                    }
                }
            } finally {
                // do this in a finally so that if an exception is thrown
                // during the above, we don't leave the Surface in an
                // inconsistent state
                if (c != null) {
                    _surfaceHolder.unlockCanvasAndPost(c);
                }
            }
            synchronized(this){
                try {
                    wait();
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block

                }
            }
        }
    }
}
}

The maintabs.xml file:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="fill_parent"
android:layout_height="fill_parent">
<TabHost xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@android:id/tabhost" android:layout_width="fill_parent"
    android:layout_height="fill_parent">
    <LinearLayout android:orientation="vertical"
        android:layout_width="fill_parent" android:layout_height="fill_parent">
        <TabWidget android:id="@android:id/tabs"
            android:layout_width="fill_parent" android:layout_height="wrap_content"
            android:layout_marginLeft="0dip" android:layout_marginRight="0dip" />
            <FrameLayout android:id="@android:id/tabcontent"
            android:layout_width="fill_parent" android:layout_height="fill_parent" />
    </LinearLayout>
    </TabHost>
</LinearLayout>

And the surfaceviewindependent.xml:

    <?xml version="1.0" encoding="utf-8"?>
<LinearLayout
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:orientation="vertical"
  android:layout_width="match_parent"
  android:layout_height="match_parent">
<uk.co.androidcontrols.gauges.SurfaceViewCircle
android:id="@+id/circle1"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_weight="0.5"
    android:layout_margin="1dip">
</uk.co.androidcontrols.gauges.SurfaceViewCircle>
</LinearLayout>

I also noted that someone else has had a similar problem here.

Apologies for the poor formatting but the code editor is nigh on impossible to use for large code quotes!

Additional Info

I tried using the setVisibility()' in onvisibilityChanged() but it eventually results in an exception:

protected void onVisibilityChanged(View changedView, int visibility){
    super.onVisibilityChanged(changedView, visibility);
    changedView.setVisibility(visibility);
    Log.i("SurfaceViewCircle", "onVisibilityChanged() called.");
}

java.lang.IllegalThreadStateException: Thread already started.

It appears calling changedView.setvisibility() destroys the surface each time.

like image 223
D-Dᴙum Avatar asked May 08 '12 21:05

D-Dᴙum


2 Answers

I built a test project based on your code and surprisingly spent almost a couple of hours fiddling about with it. I'll quickly blurt out my findings now as I ought to hit the sack!

First of all, you're most definitely creating two tabs, with each tab having a separate instance of your custom SurfaceView. That's fine.

Now when the Activity first starts up and the first tab is shown, only the first SurfaceView is initialised and has surfaceCreated() called, at which point its Thread runs.

When the second tab is selected, the second SurfaceView that the createTabContent() callback supplies for it is then initialised just as the first one was. From then on from this point, until teardown of the Activity, both SurfaceViews remain in their valid surface state. Toggling between tabs never invokes surfaceDestroyed() on either SurfaceView, and so SurfaceCreated() is never invoked again either. Neither is 'onMeasure()' invoked again after first creation. Therefore, this tells me that both SurfaceViews remain in the overall View hierarchy. Both SurfaceViews' Threads are running and, if you didn't have the wait() in there, both would be continuously trying to render to video memory.

As you know, a SurfaceView is very unique in how it sits (or rather, doesn't sit) within the View hierarchy. What seems to be happening here is that the first SurfaceView to be created is the one whose output is seen on video memory, regardless of tab selection.

One thing I first tried was to have the second SurfaceView significantly smaller than the first, with a proportionally smaller circle within. When switching from the first tab (larger SurfaceView with large red circle) to the second tab (smaller SurfaceView with smaller blue circle), I could see that the size of the visible SurfaceView reduced correctly as if the second SurfaceView was becoming visible, but rather than its smaller blue circle being visible, I had just a large portion of the first SurfaceView's large red circle punching through, but cropped by the smaller size of the second SurfaceView.

What I ended up playing with were the following two methods calls:

((SurfaceView)surfaceView1.findViewById(R.id.circle1)).setVisibility(View.GONE);

((SurfaceView)view.findViewById(R.id.circle1)).bringToFront();

The latter, bringToFront(), didn't seem to achieve anything. But using the setVisibility(View.GONE) call on the first SurfaceView just as the second SurfaceView's tab has been selected then had it nicely switching from a red circle to a blue circle.

What I think you therefore need to try doing is looking for a suitable TabHost API callback method to override which will be called every time a tab is selected (perhaps using TabHost.OnTabChangeListener) and use that as a place to call setVisibility() as appropriate on all of the SurfaceViews to control which one appears on top.

like image 57
Trevor Avatar answered Oct 30 '22 18:10

Trevor


Looks like what I want to do with SurfaceView is not recommended: link

like image 44
D-Dᴙum Avatar answered Oct 30 '22 17:10

D-Dᴙum