Background
I have an app in the Google Play store built in Kotlin.
It currently displays a grid that the user draws her password on.
Here's a snapshot of the grid as it was previously drawn with the previous default paint.strokeWidth.
The grey lines between the (red) posts are drawn with the following method:
private fun DrawGridLines() {
val paint = Paint()
for (y in 0..numOfCells) {
xCanvas!!.drawLine(
(0 + leftOffset).toFloat(), (y * cellSize + topOffset).toFloat(),
(numOfCells * cellSize + leftOffset).toFloat(),
(y * cellSize + topOffset).toFloat(), paint
)
}
for (x in 0..numOfCells) {
xCanvas!!.drawLine(
(x * cellSize + leftOffset).toFloat(), (0 + topOffset).toFloat(),
(x * cellSize + leftOffset).toFloat(), (numOfCells * cellSize + topOffset).toFloat(),
paint
)
}
}
The Problem
While working on updates to the app I ran it on the emulator and saw the following:
As you can see the gridlines are drawn properly. Very odd since it seems to be drawing partial grid lines. NOTE: I ran this on numerous API versions and they all draw the grid lines this way now.
paint.strokeWidth = 0.0
I added some code to examine the value of paint.strokeWidth but that is additionally odd. It shows that the value of strokeWidth is always 0.0.
You can see that in my logcat output:
The Fix
Yes, I can simply fix this by explicitly setting the value myself.
I added the following line of code to the routine above:
paint.strokeWidth = 5F;
Now it looks like the following:
However, I'd like to know why this has suddenly occurred??
I'd also like to know how it seems to draw "some" of the lines since the value of the strokeWidth is actually 0.0???
The first thing I see in your code is that nowhere the Paint gets configured or its strokeWidth assigned a value. You need to set specific values and not use defaults, as defaults don't take into consideration display densities neither may have a valid usable value at all.
In the next sniped of your code you instantiate a new Paint instance and use it straight away without setting any properties to it:
private fun DrawGridLines() {
val paint = Paint()
for (y in 0..numOfCells) {
xCanvas!!.drawLine(....
"Here using already the new paint??? where did you configure it?"
Secondly, notice that Paint.strokeWidth units are in pixels, therefore you need to take into account the device display density and adjust to it.
for example:
val DEFAULT_SIZE_PX = 5.0f
val scaledWidth = DEFAULT_SIZE_PX * context
.resources
.displayMetrics
.density
paint.strokeWidth = scaledWidth
Or, which is the same as:
val DEFAULT_SIZE_PX = 5.0f
val displayMetrics = context.resources.displayMetrics
val scaledWidth = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, DEFAULT_SIZE_PX, displayMetrics)
paint.strokeWidth = scaledWidth
The official docs on setStrokeWidth provides a very interesting statement:
"Hairlines always draw a single pixel..."
I suppose the way that is handled is now probably handled differently and has this type of effect on output now. Or it is related to the density issue.
Either way, it is odd that it has changed. And interesting/odd that it states that you can set it to 0 for hairline output.
Related
I am working on a pin entry screen and the pin digit orbs are being scaled once their total width becomes greater than the content space available. But the first and final orb are pushing out of the content space and I can't figure out why.
This is an exmaple of the pin orbs pushing out the edges:
Code:
padViewHolder.addView(
padView,
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT)
padView.setPadActionListener(object : PadView.PadActionListener {
override fun symbolPressed() {
val digit = ImageView(pinPadDigitContainer.context)
digit.setImageResource(R.drawable.ic_pin_digit_filled)
if (noSpace) {
noSpace = false
pinPadDigitContainer.addView(digit)
pinPadDigitContainer.resizeChildrenToFit()
} else {
val space = Space(context)
pinPadDigitContainer.addView(space)
pinPadDigitContainer.addView(digit)
pinPadDigitContainer.resizeChildrenToFit()
pinPadDigitContainer.invalidate()
}
}
private fun ViewGroup.resizeChildrenToFit() {
val numOfOrbs = (childCount - 1) / 2
val displayWidth = resources.displayMetrics.run { widthPixels / density }
val singleSpace = calculatePercentageSpace(displayWidth).toInt() // 7% of screen width
val widthOfOrb = resources.getDimensionPixelSize(R.dimen.ic_pin_digit_width) // 29dp
val dpInt = resources.getDimensionPixelSize(R.dimen.ic_pin_orb_space) // 3dp
val contentSpace = width - paddingLeft - paddingRight
var childrenWidth = numOfOrbs * widthOfOrb + (singleSpace * (numOfOrbs - 1))
if (childrenWidth <= contentSpace) {
for (i in 0 until childCount) {
calculateOrbAndSpaceWidth(i, widthOfOrb, singleSpace)
}
} else {
childrenWidth = remain
val smallOrb = contentSpace / numOfOrbs - 3
for (i in 0 until childCount) {
calculateOrbAndSpaceWidth(i, smallOrb, dpInt)
}
}
}
This is the part of the view i'm working with:
<LinearLayout
android:id="#+id/meta__display2_container"
android:layout_width="0dp"
android:minWidth="0dp"
android:layout_height="0dp"
android:minHeight="0dp"
android:gravity="center"
android:orientation="horizontal"
android:clipChildren="true"
app:layout_constraintDimensionRatio="5.5:1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#id/meta__display1_s"
app:layout_constraintWidth_percent="0.56" />
You haven't posted the actual code where you calculate the scaled widths (calculateOrbAndSpaceWidth) but it looks like that's not being worked out correctly. Your code's a little hard for me to follow, so I can't really rework it, but can't you do something like this?
val orbWidth = // get default orb width
val spaceWidth = // get default space between orbs
val totalWidth = (orbWidth * orbCount) + (spaceWidth * (orbCount-1))
val availableWidth = // get width of view - padding
// Work out what scaling factor would make all the orbs fit the available space exactly.
// If they need to be scaled down (scale: less than 1.0) use that value, otherwise keep them
// at the current size (scale: 1.0)
val scaleFactor = minOf(1f, availableWidth / totalWidth.toFloat())
And then draw all your orbs with the scale factor applied to the orb dimensions, space widths, and any coordinate offsets. (I don't know how you're actually drawing them, but you're scaling them somehow!) When scaleFactor is 1.0 it'll draw everything as normal, when it's smaller it'll adjust all your measurements to make them fit.
Also make sure you're not mixing up dp and px measurements - you seem to be working with both. Personally I'd convert any of your dp values to pixels (which you're mostly doing) and just work with that. This part here:
val displayWidth = resources.displayMetrics.run { widthPixels / density }
Is taking the width of the window in pixels and converting it to dp. You only use it in this function call:
val singleSpace = calculatePercentageSpace(displayWidth).toInt()
so I can't see what you're doing with it - but you either that result to calculateOrbAndSpaceWidth, or dpInt (which is a px value), and you're getting expected results with one and unexpected with the other. So just make sure you're being consistent in there - might be fine, just pointing it out!
Ok I am having an issue trying to put a square root equation in a calculator. I am trying to figure out the expression builder. I know the expression builder takes the math operations of add, subtract, multiply, divide, equals and the parenthesis. What I am doing is trying to build the square root section. I have a simple Percent code to help with the square root.
In the Square root vs the Percent you see I am using binding. So here is the code for both.
On the square root is it possible to use the expression builder? I know there is no absolute formula for square root except for a number that is multipliable with itself like the number 4.
Sqrt(4) = 2
binding.btnSqrt.setOnClickListener {
var square = (tv_equation.text.toString().toDouble() / 2)
binding.tvResult.text = square.toString()
}
So in the event you a non square equation
sqrt(23) = 4.79
How would I simulate that as one function within the button. Can I use expression or would I need to use Kotlin.math
So between the two I divide by 100 on the percent. It works great.
binding.btnPercent.setOnClickListener {
var percentage = (tv_equation.text.toString().toDouble() / 100)
binding.tvResult.text = percentage.toString()
}
All my other buttons work fine and so I am just working on the square root before I can release this to google play.
You would need to use some form of a square root function.
AS you mentioned in your question Kotlin Sqrt Function is a very suitable choice
binding.btnSqrt.setOnClickListener {
if(!tv_equation.text.isNullOrEmpty){
var number = tv_equation.text.toString().toDouble()
binding.tvResult.text = sqrt(number).toString()
}
You can create a sqrt function using the Quake's first inverse square root.
Quake's first inverse square root.
Pseudo Code:
float InvSqrt(float x){
float xhalf = 0.5f * x;
int i = *(int*)&x; // store floating-point bits in integer
i = 0x5f3759df - (i >> 1); // initial guess for Newton's method
x = *(float*)&i; // convert new bits into float
x = x*(1.5f - xhalf*x*x); // One round of Newton's method
return x;
}
So the answer i want to add is a little more suitable with the previous answer. Maybe this will help some people in the future. This solution will also limit the decimal space to 4 decimals.
binding.btnSqrt.setOnClickListener{
val df = DecimalFormat("#.####")
if(!tv_equation.text.isNullOrEmpty())
{
val number = tv_equation.text.toString().toDouble()
binding.tvResult.text = df.format(sqrt(number))
}
}
you can adjust the val df = DecimalFormat("#.####") where the # to as many decimals as you would want.
I subclassed TextView to provide a custom onDraw. But canvas has a clip region applied that is nonsensical: the x is set to something well outside the view bounds. I think that's thwarting my purposes.
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
// draw numberLabel
if (numberLabel == 0)
return
val right = this.width - this.resources.getDimension(R.dimen.topNavBadgeEndMargin)
// top needs to add the top margin and estimated text height
val top = this.resources.getDimension(R.dimen.topNavBadgeTopMargin) + this.badgePaint.textSize
canvas.drawText(numberLabel.toString(), right, top, this.badgePaint)
val r = Rect()
canvas.getClipBounds(r)
Log.d("TopNav", "canvas.clipBounds: $r")
}
Logcat printed:
D/TopNav: canvas.clipBounds: Rect(524187, 0 - 524389, 147)
FYI, I have also tried drawing a circle r=50 center=(100,100) and it doesn't show. So what would help is a) why this happens? b) I know there's no way to reset the clip region, but is there any workaround that would help me?
Seems like if you override onDraw in a TextView you need to offset by scrollX (probably should do scrollY as well, though it was zero). scrollX was the rediculously large int and I have no idea why it would be nonzero in a TextView that doesn't need to scroll.
val right = this.scrollX + this.width - this.resources.getDimension(R.dimen.topNavBadgeEndMargin)
If you have several operations then canvas.translate wrapped by save and restore probably helps.
I have gone through the samples for creating a custom Android 2.0 watchface with support for complications. the document for ComplicationDrawable states that we can provide custom progressbars and use setRangedValueProgressHidden() to suppress the default UI.
Optional fields are not guaranteed to be displayed. If you want to draw your own progress bar, you can use the setRangedValueProgressHidden() method to hide the progress bar provided by the ComplicationDrawable class.
But I have been unable to find guides on how to draw the custom UI after setting the default progress bar to hidden. Any pointers will be highly appreciated.
There is no guide because there isn't a single/preferred way to do this. Here are a few steps to help you get started:
1) Create a Canvas and a Bitmap that are large enough to contain your custom progress bar:
Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
2) Make sure that the complication has data to show, and that it is a ranged value complication. (You can access the complication data from the onComplicationDataUpdate(int complicationId, ComplicationData complicationData) method.):
if(complicationData != null && complicationData.getType() == ComplicationData.TYPE_RANGED_VALUE) {
// TODO: Get progress data
}
3) Get the progress values from your ComplicationData object (all these fields are required):
float minValue = complicationData.getMinValue();
float maxValue = complicationData.getMaxValue();
float currentValue = complicationData.getValue();
4) Draw the progress in whatever way you want on your Canvas. Below is a simplified example from one of our watch faces.
// Calculate the start angle based on the complication ID.
// Don't worry too much about the math here, it's very specific to our watch face :)
float startAngle = 180f + 22.5f + ((complicationId - 2) * 45f);
// Calculate the maximum sweep angle based on the number of complications.
float sweepAngle = 45;
// Translate the current progress to a percentage value between 0 and 1.
float percent = 0;
float range = Math.abs(maxValue - minValue);
if (range > 0) {
percent = (currentValue - minValue) / range;
// We don't want to deal progress values below 0.
percent = Math.max(0, percent);
}
// Calculate how much of the maximum sweep angle to show based on the current progress.
sweepAngle *= percent;
// Add an arc based on the start and end values calculated above.
Path progressPath = new Path();
progressPath.arcTo(getScreenRect(), startAngle, sweepAngle);
// Draw it on the canvas.
canvas.drawPath(progressPath, getProgressPaint());
And here is the end result:
Background
I'm developing an app for Android that plots data as a line graph using AndroidPlot. Because of the nature of the data, it's important that it be pannable and zoomable. I'm using AndroidPlot's sample code on bitbucket for panning and zooming, modified to allow panning and zooming in both X and Y directions.
Everything works as desired except that there are no X and Y axis lines. It is very disorienting to look at the data without them. The grid helps, but there's no guarantee that grid lines will actually fall on the axis.
To remedy this I have tried adding two series, one that falls on just the X axis and the other on the Y. The problem with this is that if one zooms out too far the axis simply end, and it becomes apparent that I have applied a 'hack'.
Question
Is it possible to add X and Y axis lines to AndroidPlot? Or will my sad hack have to do?
EDIT
Added tags
I figured it out. It wasn't trivial, took a joint effort with a collaborator, and sucked up many hours of our time.
Starting with the sample mentioned in my question, I had to extend XYPlot (which I called GraphView) and override the onPreInit method. Note that I have two PointF's, minXY and maxXY, that are defined in my overridden XYPlot and manipulated when I zoom or scroll.
#Override
protected void onPreInit() {
super.onPreInit();
final Paint axisPaint = new Paint();
axisPaint.setColor(getResources().getColor(R.color.MY_AXIS_COLOR));
axisPaint.setStrokeWidth(3); //or whatever stroke width you want
XYGraphWidget oldWidget = getGraphWidget();
XYGraphWidget widget = new XYGraphWidget(getLayoutManager(),
this,
new SizeMetrics(
oldWidget.getHeightMetric(),
oldWidget.getWidthMetric())) {
//We now override XYGraphWidget methods
RectF mGridRect;
#Override
protected void doOnDraw(Canvas canvas, RectF widgetRect)
throws PlotRenderException {
//In order to draw the x axis, we must obtain gridRect. I believe this is the only
//way to do so as the more convenient routes have private rather than protected access.
mGridRect = new RectF(widgetRect.left + ((isRangeAxisLeft())?getRangeLabelWidth():1),
widgetRect.top + ((isDomainAxisBottom())?1:getDomainLabelWidth()),
widgetRect.right - ((isRangeAxisLeft())?1:getRangeLabelWidth()),
widgetRect.bottom - ((isDomainAxisBottom())?getDomainLabelWidth():1));
super.doOnDraw(canvas, widgetRect);
}
#Override
protected void drawGrid(Canvas canvas) {
super.drawGrid(canvas);
if(mGridRect == null) return;
//minXY and maxXY are PointF's defined elsewhere. See my comment in the answer.
if(minXY.y <= 0 && maxXY.y >= 0) { //Draw the x axis
RectF paddedGridRect = getGridRect();
//Note: GraphView.this is the extended XYPlot instance.
XYStep rangeStep = XYStepCalculator.getStep(GraphView.this, XYAxisType.RANGE,
paddedGridRect, getCalculatedMinY().doubleValue(),
getCalculatedMaxY().doubleValue());
double rangeOriginF = paddedGridRect.bottom;
float yPix = (float) (rangeOriginF + getRangeOrigin().doubleValue() * rangeStep.getStepPix() /
rangeStep.getStepVal());
//Keep things consistent with drawing y axis even though drawRangeTick is public
//drawRangeTick(canvas, yPix, 0, getRangeLabelPaint(), axisPaint, true);
canvas.drawLine(mGridRect.left, yPix, mGridRect.right, yPix, axisPaint);
}
if(minXY.x <= 0 && maxXY.x >= 0) { //Draw the y axis
RectF paddedGridRect = getGridRect();
XYStep domianStep = XYStepCalculator.getStep(GraphView.this, XYAxisType.DOMAIN,
paddedGridRect, getCalculatedMinX().doubleValue(),
getCalculatedMaxX().doubleValue());
double domainOriginF = paddedGridRect.left;
float xPix = (float) (domainOriginF - getDomainOrigin().doubleValue() * domianStep.getStepPix() /
domianStep.getStepVal());
//Unfortunately, drawDomainTick has private access in XYGraphWidget
canvas.drawLine(xPix, mGridRect.top, xPix, mGridRect.bottom, axisPaint);
}
}
};
widget.setBackgroundPaint(oldWidget.getBackgroundPaint());
widget.setMarginTop(oldWidget.getMarginTop());
widget.setMarginRight(oldWidget.getMarginRight());
widget.setPositionMetrics(oldWidget.getPositionMetrics());
getLayoutManager().remove(oldWidget);
getLayoutManager().addToTop(widget);
setGraphWidget(widget);
//More customizations can go here
}
And that was that. I sure wish this was built into AndroidPlot; it'll be nasty trying to fix this when it breaks in an AndroidPlot update...