MPAndroidChart - is it possible to control z-index of the chart elements? - android

I would like to have the following order of drawing in my MPAndroidChart (from bottom to top):
Data connecting line
Limit line
Data points
Is it possible? I am aware of the method com.github.mikephil.charting.components.AxisBase#setDrawLimitLinesBehindData. It is working as expected except for one case. When the Y value of all data points is the same, the effect is:
or this:
I would like it to look like:
The first 2 pictures are from MPAndroidChart Android library. The 3rd one is from iOS port of the library: Charts
I looked at the order or drawing the chart in Android and iOS versions and they look the same.
Questions:
Is it possible to control the drawing order?
What comes the difference from between system versions?
Is there any other open source library that can do that?
Additional info: all images, lines, circles are drawn by the library, custom images are not used.

Similar to a previous answer here there is no public API exposed to directly set the z-index of the various drawing features.
Instead, components are drawn in order on the canvas with later components being drawn over earlier ones. This means you change the rendering order, you can change the z-index.
You say you would like the following drawing order:
Data connecting line
Limit line
Data points
Let's find the methods in the source code that deal with each of those:
Data connecting line is drawn inside LineChartRenderer in a method called:
protected drawLinear(Canvas c, ILineDataSet dataSet)
The limit line is drawn inside XAxisRenderer called:
public void renderLimitLines(Canvas c)
The data points (circles) are drawn inside LineChartRenderer in a method that looks like this:
public void drawExtras(Canvas c)
The calling order of these three methods is determined inside BarLineChartBase in the method overriden from Android's View:
protected onDraw(Canvas canvas);
So to get the order you want you will have to simply re-arrange the order of calling of the above 3 methods inside onDraw(Canvas canvas):
Here is the complete code for a custom line chart that should meet the requirement. By design, you will still have to call:
com.github.mikephil.charting.components.AxisBase#setDrawLimitLinesBehindData
But you could easily remove the 3 if statements and hard-code the order should you so desire.
CustomZIndexBarLineBase.java
import android.content.Context;
import android.graphics.Canvas;
import android.util.AttributeSet;
import com.github.mikephil.charting.charts.BarLineChartBase;
import com.github.mikephil.charting.data.LineData;
/**
* Created by David on 11/01/2017.
*/
public class CustomZIndexLineChartBase extends BarLineChartBase<LineData> {
public CustomZIndexLineChartBase(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
public CustomZIndexLineChartBase(Context context, AttributeSet attrs) {
super(context, attrs);
}
public CustomZIndexLineChartBase(Context context) {
super(context);
}
#Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (mData == null)
return;
// execute all drawing commands
drawGridBackground(canvas);
if (mAxisLeft.isEnabled())
mAxisRendererLeft.computeAxis(mAxisLeft.mAxisMinimum, mAxisLeft.mAxisMaximum, mAxisLeft.isInverted());
if (mAxisRight.isEnabled())
mAxisRendererRight.computeAxis(mAxisRight.mAxisMinimum, mAxisRight.mAxisMaximum, mAxisRight.isInverted());
if (mXAxis.isEnabled())
mXAxisRenderer.computeAxis(mXAxis.mAxisMinimum, mXAxis.mAxisMaximum, false);
mXAxisRenderer.renderAxisLine(canvas);
mAxisRendererLeft.renderAxisLine(canvas);
mAxisRendererRight.renderAxisLine(canvas);
if (mAutoScaleMinMaxEnabled) {
autoScale();
}
mXAxisRenderer.renderGridLines(canvas);
mAxisRendererLeft.renderGridLines(canvas);
mAxisRendererRight.renderGridLines(canvas);
if (mXAxis.isDrawLimitLinesBehindDataEnabled())
mXAxisRenderer.renderLimitLines(canvas);
if (mAxisLeft.isDrawLimitLinesBehindDataEnabled())
mAxisRendererLeft.renderLimitLines(canvas);
if (mAxisRight.isDrawLimitLinesBehindDataEnabled())
mAxisRendererRight.renderLimitLines(canvas);
int clipRestoreCount = canvas.save();
canvas.clipRect(mViewPortHandler.getContentRect());
mRenderer.drawData(canvas); //NOTE: draws line between points
if (valuesToHighlight())
mRenderer.drawHighlighted(canvas, mIndicesToHighlight);
canvas.restoreToCount(clipRestoreCount);
//NOTE: draws limit line
if (!mXAxis.isDrawLimitLinesBehindDataEnabled())
mXAxisRenderer.renderLimitLines(canvas);
if (!mAxisLeft.isDrawLimitLinesBehindDataEnabled())
mAxisRendererLeft.renderLimitLines(canvas);
if (!mAxisRight.isDrawLimitLinesBehindDataEnabled())
mAxisRendererRight.renderLimitLines(canvas);
mRenderer.drawExtras(canvas); //NOTE: draws circles
mXAxisRenderer.renderAxisLabels(canvas);
mAxisRendererLeft.renderAxisLabels(canvas);
mAxisRendererRight.renderAxisLabels(canvas);
if (isClipValuesToContentEnabled()) {
clipRestoreCount = canvas.save();
canvas.clipRect(mViewPortHandler.getContentRect());
mRenderer.drawValues(canvas);
canvas.restoreToCount(clipRestoreCount);
} else {
mRenderer.drawValues(canvas);
}
mLegendRenderer.renderLegend(canvas);
drawDescription(canvas);
drawMarkers(canvas);
}
}
CustomZIndexLineChart.java
import android.content.Context;
import android.graphics.Canvas;
import android.util.AttributeSet;
import com.github.mikephil.charting.data.LineData;
import com.github.mikephil.charting.interfaces.dataprovider.LineDataProvider;
import com.github.mikephil.charting.renderer.LineChartRenderer;
/**
* Created by David on 11/01/2017.
*/
public class CustomZIndexLineChart extends CustomZIndexLineChartBase implements LineDataProvider {
public CustomZIndexLineChart(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
public CustomZIndexLineChart(Context context, AttributeSet attrs) {
super(context, attrs);
}
public CustomZIndexLineChart(Context context) {
super(context);
}
#Override
protected void init() {
super.init();
mRenderer = new LineChartRenderer(this, mAnimator, mViewPortHandler);
}
#Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
}
#Override
public LineData getLineData() {
return mData;
}
#Override
protected void onDetachedFromWindow() {
// releases the bitmap in the renderer to avoid oom error
if (mRenderer != null && mRenderer instanceof LineChartRenderer) {
((LineChartRenderer) mRenderer).releaseBitmap();
}
super.onDetachedFromWindow();
}
}

Related

How do I receive a callback when drawing graph completes in MPAndroidChart?

I am having an issue where I am refreshing a LineDataSet graph real time by updating the data and calling mMyChart.invalidate().
One thread is requesting the data from a server, processing it, and sending it via a message to the second thread, which is the main activity thread and is responsible for drawing the graph.
The issue is this call to invalidate seems to complete before the chart has actually redrawn, so my other thread requesting the data goes off and requests more data and sends it to the thread to re draw the chart. The thread drawing the charts gets very behind after a while and starts taking a long time to respond to anything, the longer it has been running, the longer it takes.
My question is straight forward in that I want to make it so I don't continue requesting data until the plot has finished drawing. How do I receive a callback when drawing completes? Alternately, is there a way to detect when it is drawing?
Thanks,
Eric
You could try a ViewTreeObserver (related SO question here)
A view tree observer is used to register listeners that can be notified of global changes in the view tree. Such global events include, but are not limited to, layout of the whole tree, beginning of the drawing pass, touch mode change.... A ViewTreeObserver should never be instantiated by applications as it is provided by the views hierarchy. Refer to getViewTreeObserver() for more information.
Or you could try extending the particular Chart type you are using and override onDraw(Canvas canvas) to call a listener when the draw pass completes:
public class ExtendedBarChart extends BarChart {
public ExtendedBarChart(Context context) {
super(context);
}
public ExtendedBarChart(Context context, AttributeSet attrs) {
super(context, attrs);
}
public ExtendedBarChart(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
#Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// TODO: Drawing completed, execute callback...
}
}

ProgressBars and Espresso

When I have a ProgressBar in layouts that are displayed when running some espresso-tests - then I run into:
Caused by: android.support.test.espresso.AppNotIdleException: Looped for 1670 iterations over 60 SECONDS. The following Idle Conditions failed .
What is a nice way to work around this? Found some hackish things but searching for a nice way
If the ProgressBar is invisible when the test starts, the Drawable can be replaced with by a custom ViewAction:
// Replace the drawable with a static color
onView(isAssignableFrom(ProgressBar.class)).perform(replaceProgressBarDrawable());
// Click a button (that will make the ProgressBar visible)
onView(withText("Show ProgressBar").perform(click());
The custom ViewAction:
public static ViewAction replaceProgressBarDrawable() {
return actionWithAssertions(new ViewAction() {
#Override
public Matcher<View> getConstraints() {
return isAssignableFrom(ProgressBar.class);
}
#Override
public String getDescription() {
return "replace the ProgressBar drawable";
}
#Override
public void perform(final UiController uiController, final View view) {
// Replace the indeterminate drawable with a static red ColorDrawable
ProgressBar progressBar = (ProgressBar) view;
progressBar.setIndeterminateDrawable(new ColorDrawable(0xffff0000));
uiController.loopMainThreadUntilIdle();
}
});
}
I have the same problem. I could not figure out a totally elegant solution, but I will post my approach either.
What I tried to do is to override the indeterminateDrawable on the ProgressBar. When having a simple drawable no animation takes place and the Espresso test does not ran into the Idle issue.
Unfortunately main and androidTest are treated the same. I did not find a way to override the styles for my ProgressBar.
It now ended up in combining some ideas from https://gist.github.com/Mauin/62c24c8a53593c0a605e#file-progressbar-java and How to detect whether android app is running UI test with Espresso.
At first I created to custom ProgressBar classes, one for debug and one for release. The release version only calls the super constructors and does nothing else. The debug version overrides the method setIndeterminateDrawable. With this I could set a simple drawable instead of the animated one.
Release code:
public class ProgressBar extends android.widget.ProgressBar {
public ProgressBar(Context context) {
super(context);
}
public ProgressBar(Context context, AttributeSet attrs) {
super(context, attrs);
}
public ProgressBar(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
#TargetApi(Build.VERSION_CODES.LOLLIPOP)
public ProgressBar(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
}
Debug code:
public class ProgressBar extends android.widget.ProgressBar {
public ProgressBar(Context context) {
super(context);
}
public ProgressBar(Context context, AttributeSet attrs) {
super(context, attrs);
}
public ProgressBar(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
#TargetApi(Build.VERSION_CODES.LOLLIPOP)
public ProgressBar(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
#SuppressWarnings("deprecation")
#Override
public void setIndeterminateDrawable(Drawable d) {
if (isRunningTest()) {
d = getResources().getDrawable(R.drawable.ic_replay);
}
super.setIndeterminateDrawable(d);
}
private boolean isRunningTest() {
try {
Class.forName("base.EspressoTestBase");
return true;
} catch (ClassNotFoundException e) {
/* no-op */
}
return false;
}
}
As you can see I also added a check if my app is running an Espresso test, whereas the class I am searching for is the base of my Espresso tests.
The bad thing is that you have to update all your code to use your custom ProgressBar. But the good thing is that your release code does not have a major impact with this solution.
I have the similar issue. The test failed as early as the first call getActivity(). So the indeterminate drawable of ProgressBar have to be replaced after the activity started.
Application application = (Application)this.getInstrumentation().getTargetContext().getApplicationContext();
application.registerActivityLifecycleCallbacks(new Application.ActivityLifecycleCallbacks() {
#Override
public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
//not here, it's too early
}
#Override
public void onActivityStarted(Activity activity) {
//find the progressBar in your activity
ProgressBar progressBar = ((ProgressBar) activity.findViewById(R.id.progress_bar));
if(progressBar != null) {
//replace progress bar drawable as not animated
progressBar.setIndeterminateDrawable(new ColorDrawable(0xffff0000));
}
}
#Override
public void onActivityResumed(Activity activity) {
}
#Override
public void onActivityPaused(Activity activity) {
}
#Override
public void onActivityStopped(Activity activity) {
}
#Override
public void onActivitySaveInstanceState(Activity activity, Bundle outState) {
}
#Override
public void onActivityDestroyed(Activity activity) {
}
});
//Now you can start the activity
getActivity();
Based on Thomas R. solution, another approach is to change the drawable of the ProgressBar in the test, to avoid modifying production code.
Example:
Activity activity = startActivity();
// override progress bar infinite animation with a simple image
ProgressBar progressBar = (ProgressBar) activity.findViewById(R.id.loading_progressbar);
progressBar.setIndeterminateDrawable(activity.getDrawable(android.R.drawable.ic_lock_lock));
// click on the button that triggers the display of the progress bar
onView(withId(R.id.login_button)).perform(click());
This answer might be late. With espresso, you have to turn off animation.
On your device, under Settings > Developer options, disable the
following 3 settings:
Window animation scale, Transition animation scale, Animator duration scale
https://developer.android.com/training/testing/espresso/setup.html#set-up-environment
There is an answer at Testing progress bar on Android with Espresso by riwnodennyk
But be cautious about UIAnimator
Caution: We recommend testing your app using UI Automator only when
your app must interact with the system to fulfill a critical use case.
Because UI Automator interacts with system apps and UIs, you need to
re-run and fix your UI Automator tests after each system update. Such
updates include Android platform version upgrades and new versions of
Google Play services. As an alternative to using UI Automator, we
recommend adding hermetic tests or separating your large test into a
suite of small and medium tests. In particular, focus on testing one
piece of inter-app communication at a time, such as sending
information to other apps and responding to intent results. The
Espresso-Intents tool can help you write these smaller tests.
https://developer.android.com/training/testing/fundamentals.html#large-tests

Android - findViewById returns null

I'm following an example from a book and I can't understand why findViewById returns null.
This is my activity:
package it.mt.compass;
import android.app.Activity;
import android.os.Bundle;
import android.widget.Toast;
public class CompassActivity extends Activity {
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
CompassView cv = (CompassView)this.findViewById(R.id.compassView1);
// this crashes the application
//cv.setBearing(45);
// some debug code
Toast test_result;
if(cv == null) {
test_result = Toast.makeText(this, "1", Toast.LENGTH_SHORT);
test_result.show();
}
else {
test_result = Toast.makeText(this, "0", Toast.LENGTH_SHORT);
test_result.show();
}
// it shows 1
}
}
and this is the res/layout/main.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" >
<it.mt.compass.CompassView
android:id="#+id/compassView1"
android:layout_width="match_parent"
android:layout_height="match_parent"
/>
</LinearLayout>
Already cleaned (as suggested in other similar topics; what does "Clean" do?) the project with no luck.
Many thanks in advance.
Mirko
As requested, the constructors' code:
// Constructors
public CompassView(Context context) {
super(context);
initCompassView();
}
public CompassView(Context context, AttributeSet attrs) {
super(context);
initCompassView();
}
public CompassView(Context context, AttributeSet ats, int defaultStyle) {
super(context);
initCompassView();
}
That's the correct version (the problem was I didn't passed the parameters correctly to the superclass constructor):
// Constructors
public CompassView(Context context) {
super(context);
initCompassView();
}
public CompassView(Context context, AttributeSet attrs) {
super(context, attrs);
initCompassView();
}
public CompassView(Context context, AttributeSet ats, int defaultStyle) {
super(context, ats, defaultStyle);
initCompassView();
}
CompassView constructor implementation is incorrect. You're not passing the attributes to superclass and hence the id is lost.
Change here the superclass constructor invocation
public CompassView(Context context, AttributeSet attrs) {
super(context);
to super(context, attrs);
and
public CompassView(Context context, AttributeSet ats, int defaultStyle) {
super(context);
to super(context, attrs, defaultStyle); if the superclass has a ctor that accepts three args. Otherwise just use super(context, attrs). Oh, and rename the arg name from ats, even though the name doesn't matter.
In eclipse do:
Projects -> Clean.
Eefresh your app.
Run.
this will clear generated old R class.
I would try closing eclipse completely and then open it again. I've seen some really bizarre things like this happening. Another way you can try this is to just add a "textView" and try to do a findById on that and see if it's returning null. You could be loading the wrong xml view..
ie: your are loading a layout from one directory but it's actually loading a different view in a different directory with the same name...
add import it.mt.compass.R;
and try another View, like a Image or TextView instead

NumberFormatException with IntEditTextPreference

In my android application, I'm using a class called 'IntEditTextPreference'. This class is used when I want a user to introduce a preference as an integer.
But it has a problem. When the user leaves the field empty and press "ok", an NumberFormatException is thrown.
What could I do to avoid the user to press "ok" when the field is empty?
Thanks!
public class IntEditTextPreference extends EditTextPreference
{
public IntEditTextPreference(Context context)
{
super(context);
}
public IntEditTextPreference(Context context, AttributeSet attrs)
{
super(context, attrs);
}
public IntEditTextPreference(Context context, AttributeSet attrs, int defStyle)
{
super(context, attrs, defStyle);
}
#Override
protected String getPersistedString(String defaultReturnValue)
{
return String.valueOf(getPersistedInt(-1));
}
#Override
protected boolean persistString(String value)
{
return persistInt(Integer.valueOf(value));
}
}
You should probably still have a try/catch block around it to catch NumberFormatException. But there are many ways to do this. One way is you can use the buttons setClickable method to false and then true when the text is not null and is integer using onTextChangedListener. Or you could simply let it be clickable but check for empty string or non-integer when the button is clicked and use a warning message toast/alert/label to let the user know they have incorrect field before allowing the button to do anything else. Hope this helps!
In general if you are using a browser-based-application, you would use JavaScript/AJAX to display a button when the input is valid. This is already handled at client side.
To avoid the NumberFormatException, simply add a try-catch-block around the Integer.valueOf(value) statement.
Basically it depends on your client-framework. There might be better framework-specific solutions. Which one do you use?

Android drawing cache

Please explain how does the drawing cache work in Android. I'm implementing a custom View subclass. I want my drawing to be cached by the system. In the View constructor, I call
setDrawingCacheEnabled(true);
Then in the draw(Canvas c), I do:
Bitmap cac = getDrawingCache();
if(cac != null)
{
c.drawBitmap(cac, 0, 0, new Paint());
return;
}
Yet the getDrawingCache() returns null to me. My draw() is not called neither from setDrawingCacheEnabled(), nor from getDrawingCache(). Please, what am I doing wrong?
There's a hard limit on drawing cache size, available via the ViewConfiguration class.. My view is larger than allowed for caching.
FYI, the sources of the View class are available via the SDK Manager for some (not all) Android versions.
Hopefully this explains it.
public class YourCustomView extends View {
private String mSomeProperty;
public YourCustomView(Context context) {
super(context);
}
public YourCustomView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public YourCustomView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
public void setSomeProperty(String value) {
mSomeProperty = value;
setDrawingCacheEnabled(false); // clear the cache here
invalidate();
}
#Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// specific draw logic here
setDrawingCacheEnabled(true); // cache
}
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
...
}
}
Example code explained.
In the setSomeProperty() method call setDrawingCacheEnabled(false) to clear the cache and force a redraw by calling invalidate().
Call setDrawingCacheEnabled(true) in the onDraw method after drawing to the canvas.
Optionally place a log statement in the onDraw method to confirm it is only called once each time you call the setSomeProperty() method. Be sure to remove the log call once confirmed as this will become a performance issue.

Categories

Resources