Android Off-screen Snapshotting


#1

On the off-chance that someone else has either dealt with this or might know how to make it work, I’m trying to create an off-screen snapshot of a chart in Android. I’m using it to share a graph via email and other social networks, and I don’t want to force the user to visit the graph page first (and be stuck with that particular resolution). Rather, I’d like to create a graph, snapshot it, and use the snapshot.

On-screen snapshotting (when you can see the graph in a view on the screen) works well in Android. But I cannot get the OnSnapShotDone event to fire when the ShinobiChart object has been created by inflating a layout file. I suspect the chart object depends on view events I’m not generating.

I know that I am creating a working chart object, because I can draw the view to canvassed bitmap and save an image of the chart axes and title.

At first I thought I’d just scrape the view for the snapshot, but I guess there’s a good reason for the OnSnapShotDone method (stuff gets drawn in async threads later). Even after waiting, I’ve never been able to snap an image that includes the series using an inflated, off-screen view.

I can’t get OnSnapShotDone to get fired from an offscreen view. Can you? Ideas?

Here’s the layout for the fragment:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <fragment
        class="com.shinobicontrols.charts.ChartFragment"
        android:id="@+id/offscreenchartfragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</LinearLayout>

Here’s the relevant code, included in a class that implements the interfaces Java.Lang.Object, IShinobiChartOnSnapshotDoneListener and includes an OnSnapshotDone method:

ChartFragment cf;
Android.Views.View view;

var activity = Xamarin.Forms.Forms.Context as Activity;

if ((ChartFragment)(activity.FragmentManager.FindFragmentById (Resource.Id.offscreenchartfragment)) == null) {
        // Need to inflate the first time
	var o = activity.LayoutInflater.Inflate (Resource.Layout.offscreenGraph, null);
	cf = (ChartFragment)(activity.FragmentManager.FindFragmentById (Resource.Id.offscreenchartfragment));
	view = o;
} else {
        // Fragment already exists. Use it.
	cf = (ChartFragment)(activity.FragmentManager.FindFragmentById (Resource.Id.offscreenchartfragment));
	view = cf.View;
}
theChart = cf.ShinobiChart;

// This next part is a routine where we add axes, series
SetupChart(theChart);

// Here we set the view size
Bitmap graphBitmap = Bitmap.CreateBitmap(612, 612,Bitmap.Config.Argb8888);
Canvas graphCanvas = new Canvas(graphBitmap);
int measuredWidth = Android.Views.View.MeasureSpec.MakeMeasureSpec(graphBitmap.Width,MeasureSpecMode.Exactly);
int measuredHeight = Android.Views.View.MeasureSpec.MakeMeasureSpec(graphBitmap.Height,MeasureSpecMode.Exactly);
view.Measure(measuredWidth, measuredHeight);
view.Layout(0, 0, view.MeasuredWidth, view.MeasuredHeight);
view.Draw (graphCanvas); // When saved, the graphBitmap has axes and title, but no series
theChart.SetOnSnapshotDoneListener (this);
theChart.RequestSnapshot (); // Never fires

#2

Hello John,

I responded to you several days ago personally, did you receive my response? I just wondered how you were getting on?
Thanks,

Kai


#3

Yes, I think I did finally solve this.

I created a Xamarin.Forms View with a custom rendering class. Creating a Xamarin.Forms View means that the chart can be used on Xamarin.Forms content pages like any other component. You might want to think about creating templates for these yourself, by the way.

The key to generating OnSnapShotDone events is to have the chart be in the view hierarchy and (I’m pretty sure) be visible. When I put it completely off the screen (my first choice), I didn’t see the event firing. So I put the chart View in the right side of a horizontal StackView and just let the first few pixels of the chart (which are blank) show on the right side of the screen. Requires adjustments during rotation, of course.

All rather kludgy. In the Apple version of my app, I just create and draw the chart offscreen, then pick up the render from that context layer. I’d prefer that approach, frankly, because it’s synchronous and uncomplicated. This is one of the few areas where I prefer the iOS ShinobiChart over the Android one. The rest of the Android chart is very declarative, but the snapshotting is event-based like the iOS ShinobiChart (create an interface, wait for an event).

I’d prefer a GetSnapshot synchronous call which returns a bitmap, and which works offscreen (ideally). You would have to write the routines that trigger and capture the image once it’s done. As it stands now, developers have to do all that. Not particularly fun. Probably seems like an afterthought feature to your team, but sharing is important these days, and being restricted to the screen size of the mobile device is too limiting. A standard sharing picture might be 612 x 612, and it can be tough to find that kind of real estate on phones. Offscreen snapshotting allows you to choose any resolution, render the chart, and then share it.


#4

Hello John

I am glad that you have managed to resolve this issue. 

I would also like to thank you for sharing your solution with the community - I am sure that this information will help others.

Thanks for your comments and suggestions on ways in which we can improve the functionality of the snap shot code. We will certainly give this some consideration for future development. I would like to briefly touch upon our existing asynchronous snapshot design. We chose this approach due to the use of Open GL ES to render the actual data series. We found that using the usual Android pattern for capturing what’s currently on the screen, did not show the series itself within the captured image. 

Rest assured though John we do listen to our users and we will take your comments on board as we look to the future development roadmap.

Thanks and Kind Regards,

Kai.


#5

Kai,

Thanks for the reply. I certainly understand that the process of rendering has an inherent asynchronous nature to it. My desire was that Shinobi find a way to isolate my code from that. It’s a style/preference thing, but I’d prefer to keep functionality together rather than have it spread across event handlers. In the situations that I’m using it for (posting a graph to a social network or sharing by email), there is time to put up a progress dialog and wait while the graph is rendered.

My current solution is to always request a snapshot right away after graph creation, and then use a polling task with a non-blocking delay to let the rendering finish if necessary. OnSnapShot done saves the snapshot to a file and sets a snapshotDone flag for this purpose.

As a long-time product management guy, I get that this has a solution/workaround already, and I’m grateful for it. Thanks.

I’m just saying.


#6

John,  I am trying to do something similar on ios.  i have a solutiion that works for a small # charts, but crashes due to memory usage for larger data sets. I am clearly doing something wrong.   I posted a question on this a few days back, and have had no takers.    In your aug 26 post you refer to getting the chart from a context layer.    Any chance you could provide more details, and/or put up a snippet(s) of ios code showing how you implemented the snapshotting on ios?

Regards,

Mike