View.onDraw called with wrong Canvas - android

My app supports API level 10 (Gingerbread) upwards. One of the activities draws a chart which works perfectly on later versions, but when running a level 10 emulator I get an extra call to View.onDraw with the wrong canvas ID and this causes the screen to go blank. (It's not just the emulator - the problem was reported by someone running on their Gingerbread phone.)
Normal operation is for onDraw to be called twice - the first time from the framework, where I take the canvas ID from, and the second time from my call to invalidate(), which passes the same canvas ID. These two calls happen with the level 10 emulator, but then there is a third call with a different canvas ID - i.e. not belonging to the view, and this blanks it out.
The activity is derived from SherlockActivity to provide an action bar, and I believe this is what is causing the problem.
Relevant code from my activity class:
public class Chart extends SherlockActivity implements OnGestureListener, OnDoubleTapListener, OnScaleGestureListener
{
public static boolean mDataSet = false;
private ChartView mView;
private Menu mMenu;
private GestureDetector mDetector;
private ScaleGestureDetector mScaleDetector;
private ActionBar mActionBar;
/** Called when the activity is first created. */
#Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
mDataSet = false;
mActionBar = getSupportActionBar();
mActionBar.setHomeButtonEnabled(true);
mActionBar.setDisplayHomeAsUpEnabled(true);
mActionBar.setDisplayOptions(0, ActionBar.DISPLAY_SHOW_TITLE);
mView = new ChartView(this, mCentreLat, mCentreLong, mRadius);
setContentView(mView);
setTitle("");
Context context = getApplicationContext();
mDetector = new GestureDetector(context, this);
mScaleDetector = new ScaleGestureDetector(context, this);
}
#Override
public void onConfigurationChanged(Configuration config)
{
super.onConfigurationChanged(config);
mDataSet = false;
}
// Menu handling
#Override
public boolean onCreateOptionsMenu(Menu menu)
{
mMenu = menu;
MenuInflater inflater = getSupportMenuInflater();
inflater.inflate(R.menu.chart_menu, menu);
return true;
}
#Override
public boolean onOptionsItemSelected(MenuItem item)
{
boolean handled = true;
switch (item.getItemId())
{
case android.R.id.home:
finish();
break;
case R.id.viewOptions:
mDataSet = false;
Intent i = new Intent(getBaseContext(), ChartSettings.class);
startActivity(i);
break;
// Other menu options here...
default:
handled = false;
}
return handled;
}
Relevant code from my View class:
public class ChartView extends View
{
public ChartView(Context context, float centreLat, float centreLong, float radius)
{
super(context);
mContext = context;
mCentreLat = centreLat;
mCentreLong = centreLong;
mRadius = radius;
}
#Override
protected void onDraw(Canvas canvas)
{
// First time
// (We pick up the canvas id mCanvas from the parameter.)
// (Nothing much else relevant here, except this is where stuff
// gets initialized, then in setDataInfo(), mDataSet is set true
// and invalidate() is called at the end.)
if (!Chart.mDataSet)
{
setBackgroundColor(Color.WHITE);
mCanvas = canvas;
initPaint();
setDataInfo(); // invalidate() called at end
}
else
{
// Second call to onDraw() with same canvas id comes here,
// then an unwanted third call with a different canvas id, only
// with the Level 10 (Gingerbread) emulator.
// This is where the various lines and shapes are plotted
// on the canvas (mCanvas)
plotLinesAndShapes();
}
Please can anyone explain why the third call happens with the Gingerbread emulator (or phone)? The effect is that the screen is blanked (completely white).
By the time it gets to this function it's too late and the call cannot just be ignored - the screen goes blank as the call stack unwinds.
There is a workround - if the user chooses view options from the menu, then immediately returns to the chart, it is redrawn and behaves normally from then on.

The problem is that you are keeping a reference to the Canvas you receive as a parameter. There is no guarantee whatsoever that this Canvas instance will be valid after the current frame is done. You could for instance receive a different Canvas instance on every frame. You will typically receive a different Canvas when the View is rendered into a Bitmap (View.setDrawingCacheEnabled() for instance.)
Instead of keeping a reference in mCanvas, just pass the canvas you receive to plotLinesAndShapes().

Related

How to setContentView to a new class that extends view android

I have two classes, a class called Menu and a class called circleGame. When I run my application the Menu class is started and creates a rectangle on the screen. When the user taps the screen I would like my program to switch to my circleGame class (the circleGame class has the exact same code as my Menu class except it draws a circle rather than a rectangle). I wanted to do this using the Activity.setContentView method, but when I try using that method I get an error saying, "Cannot make a static reference to the non-static setContentView(View) from the type Activity." My code is shown below:
public class Menu extends View{
Paint blue = new Paint(), black = new Paint();
Display display;
//Width of the user's screen.
int screenWidth;
//Height of the user's screen.
int screenHeight;
public Menu(Context context) {
super(context);
blue.setColor(Color.BLUE);
WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
display = wm.getDefaultDisplay();
screenWidth = display.getWidth();
screenHeight = display.getHeight();
black.setColor(Color.BLACK);
}
#Override
protected void onDraw(Canvas canvas){
super.onDraw(canvas);
//Setting the background color to white.
canvas.drawColor(Color.WHITE);
canvas.drawRect(0, screenHeight/2, screenWidth, screenHeight/1.5f, blue);
invalidate();
}
#Override
public boolean onTouchEvent(MotionEvent event) {
float x = event.getX();
float y = event.getY();
if(event.getAction() == event.ACTION_UP){
circleGame test = new circleGame(getContext());
Activity.setContentView(test);
}
return true;
}
}
My error occurs on the line in the onTouchEvent Listener where I wrote: Activity.setContentView(test); I know I can just put the two codes together to make one large file, but then my code would get two confusing if I just have this one large file later on when I start having a lot of code.
As error states, setContentView is not static method, but rather instance method of Activity. You can only setContentView on some particular activity instance.
If you creating your Menu instance from code of activity, you can pass Activity instance in constructor, and use it later. However it would be better to redesign your code to use Observer Pattern.

Pinch-Zoom Out does nothing, In makes screen disappear

I have to be missing something obvious, since everyone on the planet seems to be able to get the whole pinch-zoom thing to work. The main difference is most folks seem to be zooming on images, whereas I've a GridLayout (android-support-v7-gridlayout) with a bunch of text views. The goal seems simple enough: Allow the user to zoom in so that they can view a portion of the full grid at a larger size.
The grid draws fine, and horizontal & vertical scrolling work fine. And then, when I do a two-finger gesture with fingers moving apart (zoom), nothing happens, despite getting the log message that onScale has been called. When I move the two fingers in (pinch), all the views disappear (though my scroll bars are left).
I've looked at Matrix transformations but haven't seen that those work on anything but an image view.
The basic layout xml:
<ScrollView
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<HorizontalScrollView
android:id="#+id/chooser_layout"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
</HorizontalScrollView>
</ScrollView>
Then in the activity
private ZoomGridLayout playerGrid;
and in onCreate:
ViewGroup gridParent = (ViewGroup) findViewById(R.id.chooser_layout);
playerGrid = new ZoomGridLayout(getBaseContext());
playerGrid.setRowCount(8);
playerGrid.setColumnCount(8);
LayoutParams layout =
new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT);
playerGrid.setDetector();
gridParent.addView(playerGrid, layout);
I then proceed to add ~150 small text views to the grid.
Here's the first take at ZoomGridLayout:
public class ZoomGridLayout extends GridLayout {
private final static String TAG = "ZGL";
private ScaleGestureDetector scaleDetector;
private Context context;
public void setDetector() {
scaleDetector = new ScaleGestureDetector(context, new ScaleGesture());
}
public ZoomGridLayout(Context c) {
super(c);
context = c;
}
#Override
public boolean onTouchEvent (MotionEvent event) {
scaleDetector.onTouchEvent(event);
return true;
}
private class ScaleGesture
extends ScaleGestureDetector.SimpleOnScaleGestureListener
{
#Override
public boolean onScale(ScaleGestureDetector detector) {
Log.v(TAG, detector.toString());
ScaleAnimation scale =
new ScaleAnimation(detector.getPreviousSpan(),
detector.getCurrentSpan(),
detector.getPreviousSpan(),
detector.getCurrentSpan());
scale.setFillAfter(true);
startAnimation(scale);
return true;
}
}
}
I've been beating my head against this for a couple days now. I'm new to Android programming, so not very familiar with the libraries, but I'm an experienced developer. Any pointers would be wonderful. TIA.
I gave up on animations (though eventually I'd like to try to make that work). Here's the new onScale method:
#Override
public boolean onScale(ScaleGestureDetector detector) {
Log.v(TAG, detector.toString());
mScaleFactor *= detector.getScaleFactor();
// Don't let the object get too small or too large.
mScaleFactor = Math.max(MIN_SCALE, Math.min(mScaleFactor, MAX_SCALE));
invalidate();
return true;
}
this is then complemented by overriding the draw method:
#Override
public void dispatchDraw(Canvas canvas) {
String disp = String.valueOf(mScaleFactor);
Log.v(TAG, "dispatch draw " + disp);
canvas.save();
canvas.scale(mScaleFactor, mScaleFactor);
super.dispatchDraw(canvas);
canvas.restore();
}
i also learned that since the parent is a ViewGroup instead of a View, that one needs to override the dispatch… methods instead of the on… methods.
there are so many different variants of this pattern out there. i hope this particular wrinkle helps someone else.

Launching Activities from a View

I was wondering what the best way to approach launching activities from other views in a modular way. I'm trying to figure out a way to tell my "button" which activity to fire off once its been selected in the 'onTouchEvent'. Currently, I have a main activity that creates and sets my view to my 'MainMenu'. My main menu defines a MenuItem class that defines a rect for drawing a button, and firing off an activity when intersected/touched/clicked. However, I'm having some difficulty firing off the activity. Below are just a few snippets of code demonstrating some of what I'm trying to achieve:
public class MainMenu extends View {
...
private Vector<MenuItem> menuItems;
private MenuItem testButton;
private MenuItem testButton2;
public MainMenu(Context context) {
...
// Create our menu buttons and load their specific images
testButton = new MenuItem(context, new OptionsMenu(), 150, 50, imgButtons, 256, 64, 0, 0);
testButton2 = new MenuItem(context, OptionsMenu.class, 150, 200, imgButtons, 256, 64, 0, 0);
// Store our buttons
menuItems = new Vector<MenuItem>(5, 2);
menuItems.add(testButton);
menuItems.add(testButton2);
}
...
#Override
public boolean onTouchEvent(MotionEvent event) {
if (event.getAction() != MotionEvent.ACTION_DOWN)
super.onTouchEvent(event);
// Create our menu item iterator
Iterator<MenuItem> menuItemsIter = menuItems.iterator();
Object element;
// Loop through our menu items, drawing them
while(menuItemsIter.hasNext()) {
element = menuItemsIter.next();
if(((MenuItem)element).HasIntersected((int)event.getX(), (int)event.getY())) {
((MenuItem)element).LaunchActivity();
}
}
return true;
}
}
class MenuItem {
...
private Context container = null; // Indicates which activity contains us
private Object startObject = null; // Which activity we'll start/execute
public MenuItem(Context context, Object object, int xPos, int yPos, Bitmap image,
int imageWidth, int imageHeight, int xOffset, int yOffset) {
...
container = context;
startObject = object;
}
...
public void LaunchActivity() {
if(startObject != null) {
Intent activity = new Intent(container, startObject.getClass());
container.startActivity(activity);
}
}
}
I tried setting my MenuItem's Object two different ways (new OptionsMenu() and OptionsMenu.class), but neither seem to work. I tried dodging the use of the MenuItem's startObject when created a new Intent, and using (container, optionsMenu.class) for parameters instead. Which didn't work either. From what I know this is the correct way to fire off an activity, but I'm guess I'm missing a step somewhere.
Also, I read a few articles/posts of people mentioning the use of callbacks, but on the Activity side instead of the View side. However, it wasn't very clear if there were built in Android callbacks I should use, or if I should create my own callbacks and setup my own system.
Any information about what I'm doing wrong, or what I could do differently to approach this differently/better would be appreciated. Thanks.
IMHO, just a Button supports sending click events to an OnClickListener, your custom View should have its own custom event interface for sending its own custom events to the controller (e.g., an activity). It is up to the controller to arrange to do something with those events, such as starting up other activities.

View is not automatically being drawed when overriding dispatchDraw

I'm trying to mirror a LinearLayout.
To get this work, I extended LinerLayout to create my own View component.
This is what it looks like:
public class FlipLayout extends LinearLayout implements Runnable {
private boolean isMirroring = true;
private Handler handler;
public FlipLayout(Context context) {
super(context);
this.setWillNotDraw(false);
}
public FlipLayout(Context context, AttributeSet attr) {
super(context, attr);
this.setWillNotDraw(false);
handler = new Handler();
handler.postDelayed(this, 30);
}
#Override
protected void dispatchDraw(Canvas canvas) {
canvas.save();
if (isMirroring) {
canvas.rotate(180, getWidth() / 2, getHeight() / 2);
}
super.dispatchDraw(canvas);
canvas.restore();
}
#Override
public boolean dispatchTouchEvent(MotionEvent event) {
if (isMirroring)
event.setLocation(getWidth() - event.getX(),
getHeight() - event.getY());
return super.dispatchTouchEvent(event);
}
#Override
public void run() {
invalidate();
handler.postDelayed(this, 30);
}
}
That class is working well, but only when implementing the Runnable interface and by calling invalidate() every few milliseconds.
If I only rotate the canvas without invalidating the view, the changes of the child views are not drawn.
Now I'm wondering what's the reason for this behaviour and if theres a way to get it working without the Runnable/Handler implementation.
If I remove the line canvas.rotate(...) the changes of the child views are drawn correctly (this is for example a progressbar which is updating itself frequently.)
I hope someone can help!
Thanks so much.
Chris
It looks normal to me that changing a Gui dynamically would not update the screen. In any Gui framework, they require the programmer to manually request the redraw.
This is because if i change 10 items in my Gui, i don't want to redraw 10 times, but only once at the end. And the framework can't guess when i'm done refactoring the Gui. So i need to explicitely call a refresh method somehow.
Alternatively, you could invalidate you Gui directly when you're done rotating your stuff, and not in a separate thread.

Android: Change APIDemo example AnimateDrawable.java to have a ClickEvent-Handler

I love the API Demo examples from the Android webpage and used the AnimateDrawable.java to
get started with a WaterfallView with several straight falling images which works great. Now I like the images to stop when they are clicked. I found out that Drawables can't handle events so I changed AnimateDrawable and ProxyDrawable to be extended from View instead and added a Click-Event-Listener and Handler on the parent WaterfallView. The animation still works great, but the handler doesn't, probably because in AnimateDrawable the whole canvas is shifted when the drawabled are animated. How can I change that example so that I can implement an event handler? Or is there a way to find out where exactly my AnimateDrawables are in the view?
So the more general question is: How to add an Event Listener / Handler to an animated View?
Here are my changes to the example above:
AnimateView and ProxyView instead of AnimateDrawable and ProxyDrawable
ProxyView extended from View and all super calls changed to mProxy
I commented out mutate()
The context is still the main Activity which is passed down in the constructors
In the constructors of AnimateView setClickable(true) and setFocusable(true) are called
And here is the important source code of the parent/main WaterfallView:
public class WaterfallView extends View implements OnClickListener {
private Context mContext;
// PictureEntry is just a value object to manage the pictures
private Vector<PictureEntry> pictures = new Vector<PictureEntry>();
public WaterfallView(Context context) {
super(context);
mContext = context;
pictures.add(new PictureEntry(context.getResources().getDrawable(R.drawable.sample_0)));
pictures.add(new PictureEntry(context.getResources().getDrawable(R.drawable.sample_1)));
pictures.add(new PictureEntry(context.getResources().getDrawable(R.drawable.sample_2)));
pictures.add(new PictureEntry(context.getResources().getDrawable(R.drawable.sample_3)));
pictures.add(new PictureEntry(context.getResources().getDrawable(R.drawable.sample_4)));
pictures.add(new PictureEntry(context.getResources().getDrawable(R.drawable.sample_5)));
pictures.add(new PictureEntry(context.getResources().getDrawable(R.drawable.sample_6)));
pictures.add(new PictureEntry(context.getResources().getDrawable(R.drawable.sample_7)));
}
#Override
protected void onDraw(Canvas canvas) {
if(!setup) {
for(PictureEntry pic : pictures) pic.setAnimation(createAnimation(pic));
setup = true;
}
canvas.drawColor(Color.BLACK);
for(PictureEntry pic : pictures) pic.getAnimateView().draw(canvas);
invalidate();
}
private Animation createAnimation(PictureEntry picture) {
Drawable dr = picture.getDrawable();
dr.setBounds(0, 0, dr.getIntrinsicWidth(), dr.getIntrinsicHeight());
Animation an = new TranslateAnimation(0, 0, -1*dr.getIntrinsicHeight(), this.getHeight());
an.setRepeatCount(-1);
an.initialize(10, 10, 10, 10);
AnimateView av = new AnimateView(mContext, dr, an);
av.setOnClickListener(this);
picture.setAnimateView(av);
an.startNow();
return an;
}
#Override
public void onClick(View v) {
Log.i("MyLog", "clicked "+v);
}
}
Are you going to be clicking widgets(buttons, checkboxes, etc)? Or do you want to be able to click anywhere? I think you want the latter. So in that case you'll need this method:
#Override
public boolean onTouchEvent(MotionEvent event) {
// do stuff with your event
}
This method is ONLY called when the event is NOT handled by a view, so I think you may have to remove some of your onClickListener stuff. Refer to here for more info. And as always, experiment.

Categories

Resources