I want to make in my app simple line plot with real time drawing. I know there are a lot of various libraries but they are too big or don't have right features or licence.
My idea is to make custom view and just extend View class. Using OpenGL in this case would be like shooting to a duck with a canon. I already have view that is drawing static data - that is first I am putting all data in float array of my Plot object and then using loop draw everything in onDraw() method of PlotView class.
I also have a thread that will provide new data to my plot. But the problem now is how to draw it while new data are added. The first thought was to simply add new point and draw. Add another and again. But I am not sure what will happen at 100 or 1000 points. I am adding new point, ask view to invalidate itself but still some points aren't drawn. In this case even using some queue might be difficult because the onDraw() will start from the beginning again so the number of queue elements will just increase.
What would you recommend to achieve this goal?
This should do the trick.
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.os.Bundle;
import android.support.v4.view.ViewCompat;
import android.support.v7.app.AppCompatActivity;
import android.util.AttributeSet;
import android.view.View;
import java.io.Serializable;
public class MainActivity
extends AppCompatActivity
{
private static final String STATE_PLOT = "statePlot";
private MockDataGenerator mMockDataGenerator;
private Plot mPlot;
#Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
if(savedInstanceState == null){
mPlot = new Plot(100, -1.5f, 1.5f);
}else{
mPlot = (Plot) savedInstanceState.getSerializable(STATE_PLOT);
}
PlotView plotView = new PlotView(this);
plotView.setPlot(mPlot);
setContentView(plotView);
}
#Override
protected void onSaveInstanceState(Bundle outState)
{
super.onSaveInstanceState(outState);
outState.putSerializable(STATE_PLOT, mPlot);
}
#Override
protected void onResume()
{
super.onResume();
mMockDataGenerator = new MockDataGenerator(mPlot);
mMockDataGenerator.start();
}
#Override
protected void onPause()
{
super.onPause();
mMockDataGenerator.quit();
}
public static class MockDataGenerator
extends Thread
{
private final Plot mPlot;
public MockDataGenerator(Plot plot)
{
super(MockDataGenerator.class.getSimpleName());
mPlot = plot;
}
#Override
public void run()
{
try{
float val = 0;
while(!isInterrupted()){
mPlot.add((float) Math.sin(val += 0.16f));
Thread.sleep(1000 / 30);
}
}
catch(InterruptedException e){
//
}
}
public void quit()
{
try{
interrupt();
join();
}
catch(InterruptedException e){
//
}
}
}
public static class PlotView extends View
implements Plot.OnPlotDataChanged
{
private Paint mLinePaint;
private Plot mPlot;
public PlotView(Context context)
{
this(context, null);
}
public PlotView(Context context, AttributeSet attrs)
{
super(context, attrs);
mLinePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mLinePaint.setStyle(Paint.Style.STROKE);
mLinePaint.setStrokeJoin(Paint.Join.ROUND);
mLinePaint.setStrokeCap(Paint.Cap.ROUND);
mLinePaint.setStrokeWidth(context.getResources()
.getDisplayMetrics().density * 2.0f);
mLinePaint.setColor(0xFF568607);
setBackgroundColor(0xFF8DBF45);
}
public void setPlot(Plot plot)
{
if(mPlot != null){
mPlot.setOnPlotDataChanged(null);
}
mPlot = plot;
if(plot != null){
plot.setOnPlotDataChanged(this);
}
onPlotDataChanged();
}
public Plot getPlot()
{
return mPlot;
}
public Paint getLinePaint()
{
return mLinePaint;
}
#Override
public void onPlotDataChanged()
{
ViewCompat.postInvalidateOnAnimation(this);
}
#Override
protected void onDraw(Canvas canvas)
{
super.onDraw(canvas);
final Plot plot = mPlot;
if(plot == null){
return;
}
final int height = getHeight();
final float[] data = plot.getData();
final float unitHeight = height / plot.getRange();
final float midHeight = height / 2.0f;
final float unitWidth = (float) getWidth() / data.length;
float lastX = -unitWidth, lastY = 0, currentX, currentY;
for(int i = 0; i < data.length; i++){
currentX = lastX + unitWidth;
currentY = unitHeight * data[i] + midHeight;
canvas.drawLine(lastX, lastY, currentX, currentY, mLinePaint);
lastX = currentX;
lastY = currentY;
}
}
}
public static class Plot
implements Serializable
{
private final float[] mData;
private final float mMin;
private final float mMax;
private transient OnPlotDataChanged mOnPlotDataChanged;
public Plot(int size, float min, float max)
{
mData = new float[size];
mMin = min;
mMax = max;
}
public void setOnPlotDataChanged(OnPlotDataChanged onPlotDataChanged)
{
mOnPlotDataChanged = onPlotDataChanged;
}
public void add(float value)
{
System.arraycopy(mData, 1, mData, 0, mData.length - 1);
mData[mData.length - 1] = value;
if(mOnPlotDataChanged != null){
mOnPlotDataChanged.onPlotDataChanged();
}
}
public float[] getData()
{
return mData;
}
public float getMin()
{
return mMin;
}
public float getMax()
{
return mMax;
}
public float getRange()
{
return (mMax - mMin);
}
public interface OnPlotDataChanged
{
void onPlotDataChanged();
}
}
}
Let me try to sketch out the problem a bit more.
You've got a surface that you can draw points and lines to, and you know how to make it look how you want it to look.
You have a data source that provides points to draw, and that data source is changed on the fly.
You want the surface to accurately reflect the incoming data as closely as possible.
The first question is--what about your situation is slow? Do you know where your delays are coming from? First, be sure you have a problem to solve; second, be sure you know where your problem is coming from.
Let's say your problem is in the size of the data as you imply. How to address this is a complex question. It depends on properties of the data being graphed--what invariants you can assume and so forth. You've talked about storing data in a float[], so I'm going to assume that you've got a fixed number of data points which change in value. I'm also going to assume that by '100 or 1000' what you meant was 'lots and lots', because frankly 1000 floats is just not a lot of data.
When you have a really big array to draw, your performance limit is going to eventually come from looping over the array. Your performance enhancement then is going to be reducing how much of the array you're looping over. This is where the properties of the data come into play.
One way to reduce the volume of the redraw operation is to keep a 'dirty list' which acts like a Queue<Int>. Every time a cell in your array changes, you enqueue that array index, marking it as 'dirty'. Every time your draw method comes back around, dequeue a fixed number of entries in the dirty list and update only the chunk of your rendered image corresponding to those entries--you'll probably have to do some scaling and/or anti-aliasing or something because with that many data points, you've probably got more data than screen pixels. the number of entries you redraw in any given frame update should be bounded by your desired framerate--you can make this adaptive, based on a metric of how long previous draw operations took and how deep the dirty list is getting, to maintain a good balance between frame rate and visible data age.
This is particularly suitable if you're trying to draw all of the data on the screen at once. If you're only viewing a chunk of the data (like in a scrollable view), and there's some kind of correspondence between array positions and window size, then you can 'window' the data--in each draw call, only consider the subset of data that is actually on the screen. If you've also got a 'zoom' thing going on, you can mix the two methods--this can get complicated.
If your data is windowed such that the value in each array element is what determines whether the data point is on or off the screen, consider using a sorted list of pairs where the sort key is the value. This will let you perform the windowing optimization outlined above in this situation. If the windowing is taking place in both dimensions, you most likely will only need to perform one or the other optimization, but there are two dimensional range query structures that can give you this as well.
Let's say my assumption about a fixed data size was wrong; instead you're adding data to the end of the list, but existing data points don't change. In this case you're probably better off with a linked Queue-like structure that drops old data points rather than an array, because growing your array will tend to introduce stutter in the application unnecessarily.
In this case your optimization is to pre-draw into a buffer that follows your queue along--as new elements enter the queue, shift the whole buffer to the left and draw just the region containing the new elements.
If it's the /rate/ of data entry that's the problem, then use a queued structure and skip elements--either collapse them as they're added to the queue, store/draw every nth element, or something similar.
If instead it's the rendering process that is taking up all of your time, consider rendering on a background thread and storing the rendered image. This will let you take as much time as you want doing the redraw--the framerate within the chart itself will drop but not your overall application responsiveness.
What I did in a similar situation is to create a custom class, let's call it "MyView" that extends View and add it to my layout XML.
public class MyView extends View {
...
}
In layout:
<?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">
<com.yadayada.MyView
android:id="#+id/paintme"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
/>
</LinearLayout>
Within MyView, override method "onDraw(Canvas canv)". onDraw gets a canvas you can draw on. In onDraw, get a Paint object new Paint() and set it up as you like. Then you can use all the Canvas drawing function, e.g., drawLine, drawPath, drawBitmap, drawText and tons more.
As far as performance concerns, I suggest you batch-modify your underlying data and then invalidate the view. I think you must live with full re-draws. But if a human is watching it, updating more than every second or so is probably not profitable. The Canvas drawing methods are blazingly fast.
Now I would suggest you the GraphView Library. It is open source, don't worry about the license and it's not that big either (<64kB). You can clean up the necessary files if you wish to.
You can find a sample of usages for real time plots
in the official samples - https://github.com/jjoe64/GraphView-Demos/
Balanduino project - https://github.com/TKJElectronics/BalanduinoAndroidApp/blob/master/src/com/tkjelectronics/balanduino/GraphFragment.java
From the official samples:
public class RealtimeUpdates extends Fragment {
private final Handler mHandler = new Handler();
private Runnable mTimer1;
private Runnable mTimer2;
private LineGraphSeries<DataPoint> mSeries1;
private LineGraphSeries<DataPoint> mSeries2;
private double graph2LastXValue = 5d;
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View rootView = inflater.inflate(R.layout.fragment_main2, container, false);
GraphView graph = (GraphView) rootView.findViewById(R.id.graph);
mSeries1 = new LineGraphSeries<DataPoint>(generateData());
graph.addSeries(mSeries1);
GraphView graph2 = (GraphView) rootView.findViewById(R.id.graph2);
mSeries2 = new LineGraphSeries<DataPoint>();
graph2.addSeries(mSeries2);
graph2.getViewport().setXAxisBoundsManual(true);
graph2.getViewport().setMinX(0);
graph2.getViewport().setMaxX(40);
return rootView;
}
#Override
public void onAttach(Activity activity) {
super.onAttach(activity);
((MainActivity) activity).onSectionAttached(
getArguments().getInt(MainActivity.ARG_SECTION_NUMBER));
}
#Override
public void onResume() {
super.onResume();
mTimer1 = new Runnable() {
#Override
public void run() {
mSeries1.resetData(generateData());
mHandler.postDelayed(this, 300);
}
};
mHandler.postDelayed(mTimer1, 300);
mTimer2 = new Runnable() {
#Override
public void run() {
graph2LastXValue += 1d;
mSeries2.appendData(new DataPoint(graph2LastXValue, getRandom()), true, 40);
mHandler.postDelayed(this, 200);
}
};
mHandler.postDelayed(mTimer2, 1000);
}
#Override
public void onPause() {
mHandler.removeCallbacks(mTimer1);
mHandler.removeCallbacks(mTimer2);
super.onPause();
}
private DataPoint[] generateData() {
int count = 30;
DataPoint[] values = new DataPoint[count];
for (int i=0; i<count; i++) {
double x = i;
double f = mRand.nextDouble()*0.15+0.3;
double y = Math.sin(i*f+2) + mRand.nextDouble()*0.3;
DataPoint v = new DataPoint(x, y);
values[i] = v;
}
return values;
}
double mLastRandom = 2;
Random mRand = new Random();
private double getRandom() {
return mLastRandom += mRand.nextDouble()*0.5 - 0.25;
}
}
If you already have a view that draws static data then you're close to your goal.
The only thing you then have to do is:
1) Extract the logic that retrieves data
2) Extract the logic that draws this data to screen
3) Within the onDraw() method, first call 1) - then call 2) - then call invalidate() at the end of your onDraw()-method - as this will trigger a new draw and view will update itself with the new data.
I am not sure what will happen at 100 or 1000 points
Nothing, you do not need to worry about it. There are already a lot of points being plot every time there is any activity on the screen.
The first thought was to simply add new point and draw. Add another and again.
This is the way to go I feel. You may want to take a more systematic approach with this:
check if the plot will be on the screen or off the screen.
if they will be on the the screen simply add it to your array and that should be good.
if the data is not on the screen, make appropriate coordinate calculations considering the following: remove the first element from the array, add the new element in the array and redraw.
After this postinvalidate on your view.
I am adding new point, ask view to invalidate itself but still some points aren't drawn.
Probably your points are going off the screen. Do check.
In this case even using some queue might be difficult because the onDraw() will start from the beginning again so the number of queue elements will just increase.
This should not be a problem as the points on the screen will be limited, so the queue will hold only so many points, as previous points will be deleted.
Hope this approach helps.
Related
currently I am trying to make an animation where some fish move around. I have successfully add one fish and made it animate using canvas and Bitmap. But currently I am trying to add a background that I made in Photoshop and whenever I add it in as a bitmap and draw it to the canvas no background shows up and the fish starts to lag across the screen. I was wondering if I needed to make a new View class and draw on a different canvas or if I could use the same one? Thank you for the help!
Here is the code in case you guys are interested:
public class Fish extends View {
Bitmap bitmap;
float x, y;
public Fish(Context context) {
super(context);
bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.fish1);
x = 0;
y = 0;
}
#Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawBitmap(bitmap, x, y, null);
if (x < canvas.getWidth())
{
x += 7;
}else{
x = 0;
}
invalidate();
}
}
You can draw as many bitmaps as you like. Each will overlay the prior. Thus, draw your background first, then draw your other images. Be sure that in your main images, you use transparent pixels where you want the background to show through.
In your code, don't call Invalidate() - that's what causes Android to call onDraw() and should only be called from somewhere else when some data has changed and needs to be redrawn.
You can do something like this, where theView is the view containing your animation:
In your activity, put this code in onCreate()
myAnimation();
Then
private void myAnimation()
{
int millis = 50; // milliseconds between displaying frames
theView.postDelayed (new Runnable ()
{
#Override public void run()
{
theView.invalidate();
myAnimation(); // you can add a conditional here to stop the animation
}
}, millis);
}
I want to change background image of a menu every x-amount of seconds. I'm using libGDX scene2D.ui for making the menu. The TestScreen class extends the AbstractScreen which is a abstract class that implements libGDX's Screen class.
Problem: After I load the Image to the stage through a Table object on a stack, changing the image reference to a different image does nothing. Stage.draw() gives no cares as if it made a copy of my original image. I would like to keep the background as Image class and render through stage.draw().
To further complicate things, if I do change the image to another in render() method, then image.setVisible(false) also stops working.
public class TestScreen extends AbstractScreen {
private Stage stage;
private Image background;
private boolean ChangeBackground = true;
private final float refreshTime = 2.0f; // refresh to new image every 2 seconds.
private float counter = refreshTime;
public TestScreen(Game game) {
super(game);
}
#Override
public void render(float deltaTime) {
Gdx.gl.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
Gdx.gl.glClear(GL10.GL_COLOR_BUFFER_BIT);
if(ChangeBackground){
counter -= deltaTime;
if(counter < 0){
counter = refreshTime;
// Assets class has the 12 images loaded as "Image" objects already.
// I simple want to change the reference to other (already loaded in memory images) ...
// and make stage render the new image.
background = Assets.instance.wallpapers[(int) (Math.random()*12)]; // The image should change.
//background.setVisible(false);
}
}
stage.act(deltaTime);
stage.draw();
}
#Override
public void resize(int width, int height) {
stage.setViewport(Constants.VIEWPORT_GUI_WIDTH, Constants.VIEWPORT_GUI_HEIGHT, false);
}
#Override
public void show() {
stage = new Stage();
Gdx.input.setInputProcessor(stage);
makeStage();
}
#Override
public void hide() {
stage.dispose();
}
#Override
public void pause() {
}
// Builds the Background later and adds it to a stage through a stack.
// This is how it's done in my game. I made this test bench to demonstrate.
private void makeStage() {
Table BackGroundLayer = new Table();
background = Assets.instance.wallpapers[(int) (Math.random()*12)];
BackGroundLayer.add(background);
Stack layers = new Stack();
layers.setSize(800, 480);
layers.add(BackGroundLayer);
stage.clear();
stage.addActor(layers);
}
}
Image is a subclass of Actor. The main difference is, that Image has a Drawable inside. This Drawable gets drawn if you call stage.draw(), which calls the draw() of Image. Instead of changing Image you can change the Drawable by using setDrawable(Drawable param);.
What is a Drawable? It is any class, which implements the Drawable interface, for example the TextureRegionDrawable. If you are using TextureRegions you can use this constructor: TextureRegionDrawable(TextureRegion region);. Maybe it would be better to store your background images in a Drawable Array so you don't have to call a construcor each time you set a new Drawable. Example code:
TextureRegionDrawable[] images = new TextureRegionDrawable[12];
for (int i = 0; i<12; i++) {
images[i] = new TextureRegionDrawable(Assets.instance.textureRegions[i]);
}
Then in your render:
if(changeBackground) {
counter -= delta;
if (counter < 0) {
counter = refreshtime
background.setDrawable(images[(int)(Math.random()*12)]);
}
}
This should work
I know multiple ways to get location values of a View.
getLocationOnScreen()
getLocationInWindow()
getLeft()
However, none of them actually returns the current location of the View I moved by startAnimation() method, but only the original location.
So, now let's make a View that moves to the right by 10 pixels on each Click (I'm omitting the layout, since you can just place whatever view in your main XML and give it onClickListener).
public class AndroidTestActivity extends Activity implements OnClickListener {
LinearLayout testView;
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
testView = (LinearLayout) this.findViewById(R.id.test);
testView.setOnClickListener(this);
}
public void onClick(View v) {
int[] pLoS = new int[2];
testView.getLocationOnScreen(pLoS);
TranslateAnimation move = new TranslateAnimation(pLoS[0], pLoS[0] + 10, 0f, 0f);
move.setFillAfter(true);
move.setFillEnabled(true);
testView.startAnimation(move);
}
}
As you see, this doesn't work as I intended, since getLocationOnScreen() always returns the same value (in my case, 0), and doen't reflect the value I used in TranslateAnimation...
Any idea?
Assuming you're using Android < 3.0 then your question may be in a similar vein to mine I asked here. Basically Animations are separate from the View itself i.e. Android animates a copy of your View. That is why getLocationOnScreen() always returns 0. It's not the view that has moved (animated) it was the copy that moved (animated). If you see the answers to my question this issue has been addressed in later versions of Android.
Well, if you are trying to see how many pixels the view has shifted during/after its animation, then you can open up the Transformation object on the animation.
Here's an example using the AnimationListener:
animation.setAnimationListener(new Animation.AnimationListener() {
#Override
public void onAnimationStart(Animation animation) {
}
#Override
public void onAnimationEnd(Animation animation) {
Transformation trans = new Transformation();
// startTime + duration = end of animation
int endTime = animation.getStartTime()+animation.getDuration();
animation.getTransformation(endTime, trans);
Matrix transformationMatrix = trans.getMatrix();
float[] matrixVals = new float[9];
transformationMatrix.getValues(matrixVals);
float xTraveled = matrixVals[2];
float yTraveled = matrixVals[5];
// do something with them here...
}
#Override
public void onAnimationRepeat(Animation animation) {
}
});
I may not be exactly correct in the array indices for these, definitely use Matrix.toString() and look at the values yourself. xTraveled and yTraveled will give you the amount of distance traveled by the TranslateAnimation at the indicated time (in this case, at the end of the animation).
animations on android gingerbread and below do not really change the view in any way , only change the way it is shown.
the only way to get the new position is by calculating it.
I don't understand why invalidate(Rect) is invalidating the entire region.
The region is divided up into 5 sections and a line graph is being drawn in each one. Each section contains 100 points. As the data arrives for times tn to tn+100 I call invalidate(new Rect(left, top, right bottom)) where top is the top of the screen in height (but a lower numerical value than bottom). This invokes a call to the onDraw() method. The region from tn to tn+100 is drawn, but the previously drawn segment in region tn-100 to tn is erased. It continues that way forever. Each invalidate draws in only that region (since that is the only data I have) which IS correct, but all the previously drawn data is erased. So I get a marching segment!
In other words, I get identical behavior if I call invalidate() or invalidate(Rect).
I am assuming that the parameters of the Rect() are pixels and are getting the values based upon the height and width of the AlertDialog window in which this is being drawn.
The hope is eventually to reduce the region of 'Rect()' so I can simulate real time drawing and only invalidate time step t to t+1 instead of a region.
I must be doing something stupid.
I hope that the fact it is being done in an AlertDialog is not the issue.
This part is for trying to help 'android developer' help a noob like me get this right.
First the sequence of events:
1. Data is received via Bluetooth in a callback
2. If it is the right type of data, a BroadcastReceiver in the main activity (UI thread) is signaled and from there a routine is called that sets the parameters of a WaveFormView extends View class and then ShowDialog(id) is called which calls the onCreateDialog(id) callback in the main activity.
3. Then I call invalidate().
4. The dialog pops up and then the graph is drawn.
5. All subsequent calls to ShowDialog(id) bypass the onCreateDialog(id)
That works but the entire region is always invalidated regardless of the parameters. There are also no user events here. From your example the best I could come up with is the following where I place invalidate in the onShow() instead of calling myself after the showDialog(id)
#Override
protected Dialog onCreateDialog(int id)
{
Log.d(TAG, "Alert Dialog 'onCreateDialog' method has been called with id " + id);
Builder bldr = new AlertDialog.Builder(this);
AlertDialog alert = bldr.setView(waveForm).setNegativeButton("Dismiss " + id,
new DialogInterface.OnClickListener()
{
public void onClick(DialogInterface dialog, int id)
{
dialog.cancel();
}
}).create();
// I tried adding this and invalidating here worked only first pass
alert.setOnShowListener(
new DialogInterface.OnShowListener()
{
#Override
public void onShow(DialogInterface dialog)
{
// call to invalidate
waveForm.drawArea();
}
});
//alert.getWindow().setLayout(alert.getWindow().getAttributes().width, alert.getWindow().getAttributes().height / 2);
alert.getWindow().setLayout(waveForm.getCurrentWidth(), waveForm.getCurrentHeight());
return alert;
}
However the onShow() does not get called.
The method in the main activity that calls the showDialog is
private void displayRtsa(int[] rtsaReceived)
{
// rtsaReceived[0] has the agentsink hash code
int agent = rtsaReceived[0];
// rtsaReceived[1] has the index of the RtsaData object updated
int index = rtsaReceived[1];
TreeMap<Integer, RtsaData[]> agentRtsa = BluetoothPanService.getAgentRtsaMap();
RtsaData[] rtsaDataValues = agentRtsa.get(agent);
int dialogId = 0;
synchronized(dialogIds)
{
int i = 0;
if(dialogIds.containsKey(agent + index) == false)
{
for(i = 0; i < dialogIds.size(); i++)
{
if(dialogIds.containsValue(i) == false)
{
break;
}
}
dialogIds.put(agent + index, i);
}
dialogId = dialogIds.get(agent + index);
}
final int id = dialogId;
Log.d(TAG, "Dialog id being shown = " + dialogId);
waveForm.setPeriod(rtsaDataValues[index].getPeriod());
waveForm.setMaxMin(rtsaDataValues[index].getMinValue(), rtsaDataValues[index].getMaxValue());
waveForm.setColor(Color.argb(255, 255, 200, 0));
waveForm.setData(rtsaDataValues[index].getData());
waveForm.setTitle(rtsaDataValues[index].getType());
showDialog(id);
// invalidate
// waveForm.drawArea(); (try to do in onCreateDialog callback)
}
This is probably a completely wrong approach. Probably openGl is the only way.
By the way, thanks for putting up with me!
i think it depends on what exactly you do the invalidation on . if the view you are calling the invalidation on didn't handle a rectangular invalidation , the default invalidation takes place.
in any case , if you wish to change the behavior , you can change it yourself. for the "onDraw" method , use the next code in order to fetch the invalidated rectangle :
public class InvalidateTestActivity extends Activity
{
static class CustomView extends ImageView
{
public CustomView(Context context)
{
super(context);
}
#Override
protected void onDraw(Canvas canvas)
{
final Rect r = canvas.getClipBounds();
Log.d("DEBUG", "rectangle of invalidation:" + r);
super.onDraw(canvas);
}
}
/** Called when the activity is first created. */
#Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
CustomView customView = new CustomView(this);
customView.setLayoutParams(new FrameLayout.LayoutParams(200, 200));
customView.setBackgroundColor(0xffff0000);
customView.setOnClickListener(new OnClickListener() {
#Override
public void onClick(View v)
{
v.invalidate(new Rect(0, 0, 49, 49));
}
});
setContentView(customView);
}
}
If you have hardware acceleration enabled it looks like it just invalidates the whole view...
ViewGroup:
public final void invalidateChild(View child, final Rect dirty) {
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null && attachInfo.mHardwareAccelerated) {
// HW accelerated fast path
onDescendantInvalidated(child, child);
return;
}
I'm trying to display views which coordinates on screen depends on sensors data. I managed to show a text as explained in that SO question and I'm now trying to show some TextViews instead, but when calling invalidate() on my TextViews, they're not drawn...
Anybody can help?
Thanks
ARView.java
public class ARView extends TextView
{
public String name;
public float azimuth;
public float distance;
public float inclination;
public Location location;
public ARView(Context context)
{
super(context);
setText(name);
}
}
ARActivity.java
Here I add my ARView to its parent view.
public class ARActivity extends Activity{
private ARLayout ar;
private CBCameraView cv;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ar = new ARLayout(getApplicationContext());
cv = new CBCameraView(this.getApplicationContext());
FrameLayout rl = new FrameLayout(this.getApplicationContext());
rl.addView(cv, GlobalVars.screenWidth, GlobalVars.screenHeight);
rl.addView(ar, GlobalVars.screenWidth, GlobalVars.screenHeight);
setContentView(rl);
Location location = new Location("me");
location.setLatitude(41.371974);
location.setLongitude(2.166978);
ARView fs = new ARView(this.getApplicationContext());
fs.azimuth = 0;
fs.inclination = 0;
fs.location = location;
fs.name="Bar seco";
ar.addARView(fs);
}
}
ARLayout.java
Here I get sensors data, update my ARViews coordinates and hopefully draw them...
public class ARLayout extends View implements LocationListener, SensorEventListener
{
// ...
#Override
public void onSensorChanged(SensorEvent event) {
// here I get sensors data and update my views
for(int i=0; i<nArViews; i++)
{
ARView view = arViews.get(i);
int x = ...; // new x coordinate of my ARView
int y = ...; // new y coordinate of my ARView
view.layout(x, y, view.getBottom(), view.getRight()); // set new coordinates to my ARViews
}
invalidate(); // draw my ARLayout
}
public void onDraw(Canvas c)
{
for(int i=0; i<nArViews; i++)
{
ARView view = arViews.get(i);
view.invalidate(); // I thought it would draw my ARViews, but it doesn't :(
}
}
// more stuff
}
I don't know if this is the solution to the specific problem, but I do know this is another problem.
Remove the invalidate() call in the onDraw() method. You only need to call invalidate once per draw. Removing it from onDraw() will ensure the screen only updates when you have new sensor data. The screen won't actually draw until onDraw() leaves as well.
Alternatively, you can move the call to invalidate() AFTER the for-loop in onDraw() and remove it entirely from onSensorChanged(). This will make the screen constantly redraw whether you have sensor data or not.
It seems that you are only changing the view's positions. You don't have to call invalidate and you don't have to overrule onDraw. For those kind of things the OS knows when to redraw. Invalidate and onDraw is for custom drawing only.