Why are drawables being pushed passed the edges of layout? - android

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!

Related

Why is Android Paint strokeWidth default value suddenly different?

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.

Reduce tracking window using google mlkit vision samples

I would like to reduce the reduce bar code tracking window when using the google vision api. There are some answers here but they feel a bit outdated.
I'm using google's sample: https://github.com/googlesamples/mlkit/tree/master/android/vision-quickstart
Currently, I try to figure out if a barcode is inside my overlay box inside BarcodeScannerProcessor onSuccess callback:
override fun onSuccess(barcodes: List<Barcode>, graphicOverlay: GraphicOverlay) {
if(barcodes.isEmpty())
return;
for(barcode in barcodes) {
val center = Point(graphicOverlay.imageWidth / 2, graphicOverlay.imageHeight / 2)
val rectWidth = graphicOverlay.imageWidth * Settings.OverlayWidthFactor
val rectHeight = graphicOverlay.imageHeight * Settings.OverlayHeightFactor
val left = center.x - rectWidth / 2
val top = center.y - rectHeight / 2
val right = center.x + rectWidth / 2
val bottom = center.y + rectHeight / 2
val rect = Rect(left.toInt(), top.toInt(), right.toInt(), bottom.toInt())
val contains = rect.contains(barcode.boundingBox!!)
val color = if(contains) Color.GREEN else Color.RED
graphicOverlay.add(BarcodeGraphic(graphicOverlay, barcode, "left: ${barcode.boundingBox!!.left}", color))
}
}
Y-wise it works perfectly, but the X values from barcode.boundingBox e.g. barcode.boundingBox.left seems to have an offset. Is it based on what's being calculated in GraphicOverlay?
I'm expecting the value below to be close to 0, but the offset is about 90 here:
Or perhaps it's more efficient to crop the image according to the box?
Actually the bounding box is correct. The trick is that the image aspect ratio doesn't match the viewport aspect ratio so the image is cropped horizontally. Try to open settings (a gear in the top right corner) and choose an appropriate resolution.
For example take a look at these two screenshots. On the first one the selected resolution (1080x1920) matches my phone resolution so the padding looks good (17px). On the second screenshot the aspect ratio is different (1.0 for 720x720 resolution) therefore the image is cropped and the padding looks incorrect.
So the offset should be transformed from image coordinates to the screen coordinates. Under the hood GraphicOverlay uses a matrix for this transformation. You can use the same matrix:
for(barcode in barcodes) {
barcode.boundingBox?.let { bbox ->
val offset = floatArrayOf(bbox.left.toFloat(), bbox.top.toFloat())
graphicOverlay.transformationMatrix.mapPoints(offset)
val leftOffset = offset[0]
val topOffset = offset[1]
...
}
}
The only thing is that the transformationMatrix is private, so you should add a getter to access it.
As you know, the preview size of the camera is configurable at the settings menu. This configurable size specifies the graphicOverlay dimensions.
On the other hand, the aspect ratio of the CameraSourcePreview (i.e. preview_view in activity_vision_live_preview.xml) which is shown on the screen, does not necessarily equal to the ratio of the graphicOverlay. Because depends on the size of the phone's screen and the height that the parent ConstraintLayout allows occupying.
So, in the preview, based on the difference between the aspect ratio of graphicOverlay and preview_view, some part of the graphicOverlay might not be shown horizontally or vertically.
There are some parameters inside GraphicOverlay that can help us to adjust the left and top of the barcode's boundingBox in such a way that they start from 0 in the visible area.
First of all, they should be accessible out of the GraphicOverlay class. So, it's just enough to write a getter method for them:
GraphicOverlay.java
public class GraphicOverlay extends View {
...
/**
* The factor of overlay View size to image size. Anything in the image coordinates need to be
* scaled by this amount to fit with the area of overlay View.
*/
public float getScaleFactor() {
return scaleFactor;
}
/**
* The number of vertical pixels needed to be cropped on each side to fit the image with the
* area of overlay View after scaling.
*/
public float getPostScaleHeightOffset() {
return postScaleHeightOffset;
}
/**
* The number of horizontal pixels needed to be cropped on each side to fit the image with the
* area of overlay View after scaling.
*/
public float getPostScaleWidthOffset() {
return postScaleWidthOffset;
}
}
Now, it is possible to calculate the left and top difference gap using these parameters like the following:
BarcodeScannerProcessor.kt
class BarcodeScannerProcessor(
context: Context
) : VisionProcessorBase<List<Barcode>>(context) {
...
override fun onSuccess(barcodes: List<Barcode>, graphicOverlay: GraphicOverlay) {
if (barcodes.isEmpty()) {
Log.v(MANUAL_TESTING_LOG, "No barcode has been detected")
}
val leftDiff = graphicOverlay.run { postScaleWidthOffset / scaleFactor }.toInt()
val topDiff = graphicOverlay.run { postScaleHeightOffset / scaleFactor }.toInt()
for (i in barcodes.indices) {
val barcode = barcodes[i]
val color = Color.RED
val text = "left: ${barcode.boundingBox!!.left - leftDiff} top: ${barcode.boundingBox!!.top - topDiff}"
graphicOverlay.add(MyBarcodeGraphic(graphicOverlay, barcode, text, color))
logExtrasForTesting(barcode)
}
}
...
}
Visual Result:
Here is the visual result of the output. As it's obvious in the pictures, the gap between both left & top of the barcode and the left and top of the visible area is started from 0. In the case of the left picture, the graphicOverlay is set to the size of 480x640 (aspect ratio ≈ 1.3334) and for the right one 360x640 (aspect ratio ≈ 1.7778). In both cases, on my phone, the CameraSourcePreview has a steady size of 1440x2056 pixels (aspect ratio ≈ 1.4278), so it means that the calculation truly reflected the position of the barcode in the visible area.
(note that the aspect ratio of the visible area in one experiment is lower than that of graphicOverlay, and in another experiment, greater: 1.3334 < 1.4278 < 1.7778. So, the left values and top values are adjusted respectively.)

How to completely fit an AR ViewRenderable on target image?

I want an AR ViewRenderable to be placed over the real world target picture and completely fit over its boundaries.
Although ViewSizer is used to change the size of AR objects, but it does it globally(sets dpPerMeter for every situation). However, I want to scale it wrt target's size,
I think something can be done by setLocalScale methods along with getExtentX and getExtentZ, but not sure how to set the parameters for my cause.
Have a look at the current code snippet :
setAnchor(image.createAnchor(image.getCenterPose()));
Node cornerNode = new Node();
cornerNode.setParent(this);
cornerNode.setLocalRotation(new Quaternion(new Vector3(1,0,0), -90));
cornerNode.setLocalPosition(new Vector3(0.0f, 0.0f, 0f));
cornerNode.setRenderable(targetImage.getNow(null));
By default, 250dp equals to 1 meters, but you can change it by ViewRenderable.builder().setSizer(DpToMetersViewSizer(you_size))
you have to calculate the scale by the image size and AR core estimated size, so add these code like
// in this example, the image is 100 cm x 66 cm
val imageWidth = 1f // = 1m
val imageHeight = 0.66 // = 66 cm
val scaledWidth = imageWidth / image.extentX
val scaledHeight = imageHeight / image.extentZ
// scale the Node
node.localScale = Vector3(scaledWidth, scaledHeight, scaledWidth)
// also, my view_wall.xml is 250dp x 166dp and the VerticalAlignment is center, like
val wall = ViewRenderable.builder().setView(this, R.layout.view_wall)
.setVerticalAlignment(ViewRenderable.VerticalAlignment.CENTER)
.build()
It works for me, help it works for you.
by the way this is my practice project https://github.com/swarmnyc/arcore-augmented-image-swarm

Phaser: Make background screen-width instead of content-width

I am making a game using the Phaser Framework for Android. I need to make the canvas fill the entire screen width and then flood it with background color, then put content in the center of screen. Currently, even though I already set the width of the canvas to window width, Phaser still reset it to content width, which is smaller, then filling only the content width with background.
How do I stop Phaser from using content width?
var gameObj = new Phaser.Game($(".parent").width(), 700, Phaser.CANVAS, 'parent');
gameObj.stage.backgroundColor = '#A6E5F5';
gameObj.load.image('prld', 'prl.png');
gameObj.scale.scaleMode = Phaser.ScaleManager.SHOW_ALL;
gameObj.scale.pageAlignHorizontally = true;
gameObj.scale.pageAlignVertically = true;
gameObj.scale.setScreenSize(true);
gameObj.scale.refresh();
gameObj.stage.backgroundColor = '#A6E5F5';
// Load Content Here
gameObj.load.image('h1', 'res1/h1.png');
gameObj.load.image('h2', 'res1/h2.png');
....
g = gameObj.add.group();
g.create(20, 20, 'h1');
g.create(60, 20, 'h2');
....
You need Phaser 2.2+ for this, but if you want the canvas to fill the entire screen space available you would do:
var game = new Phaser.Game("100%", "100%", Phaser.CANVAS, 'parent');
...
game.scale.scaleMode = Phaser.ScaleManager.RESIZE;
Also you should not use pageAlign if you're using 100% dimensions. You also don't need to call refresh or setScreenSize.
When working in the RESIZE scale mode you can add a function to your States called 'resize'. It will be called whenever the screen dimensions change (orientation event perhaps) and be passed 2 parameters: width and height. Use those to adjust your game content layout.
You may want to get the Phaser Scale Manager book, it covers all this in lots more detail (caveat: I wrote it, but it has a "pay what you want" price, including completely free)
i use this code in my game and it work very nice without changing in position of any elements
var targetWidth = 720; // the width of the game we want
var targetHeight = 480; // the height of the game we want
// additional ratios
//Small – 360x240
//Normal – 480x320
//Large – 720x480
//XLarge – 960x640
//XXLarge – 1440x960
var deviceRatio = (window.innerWidth/window.innerHeight); //device aspect ratio
var newRatio = (targetHeight/targetWidth)*deviceRatio; //new ratio to fit the screen
var newWidth = targetWidth*newRatio;
var newHeight = targetHeight;
var gameWidth = newWidth;
var gameHeight = newHeight;
var gameRendrer = Phaser.AUTO;
and this code for Boot state
game.scale.scaleMode = Phaser.ScaleManager.SHOW_ALL;
game.scale.pageAlignHorizontally = true;
game.scale.pageAlignVertically = true;
game.scale.forceLandscape = true;
game.scale.setScreenSize(true);
this for landscape mode if you want to make it for portrait change width and height like
var targetWidth = 480; // the width of the game we want
var targetHeight = 720; // the height of the game we want
also force it to portrait
this code work well in my games

RelativeLayout size and position (margins)

I want my RelativeLayouts be at the same size and position independently of the size of the device screen (relatively). For example:
If device A has a screen width of 480 and height of 800 and device B has a screen width of 720 and height of 1280, my RelativeLayout must have, for example, 160 width and 267 height in device A and 240 width and 427 height in device B. It's simple, just get the screen width and height and calculate the percent you want. This part I did with this code below and it's working:
int width = methodToGetScreenWidth();
relativeOneWidth = ((width / 100) * 90); // 90% of the screen width.
relativeOneHeight = (int) (relativeOneWidth / 1.75f);
relativeTwoWidth = (relativeOneWidth / 2);
relativeTwoHeight = (relativeOneHeight / 4);
relativeThreeWidth = (relativeOneWidth / 4);
relativeThreeHeight = (relativeOneHeight / 4);
It's working fine in all devices I tested. The problem is the position. I tried a lot of things to calculate the margins, but it didn't be at the same position in every devices (most times it's very close to the desired position, but not the exactly). Here's what I tried:
relativeOneLeftMargin = ((width - relativeOneWidth) / 2); // The screen width - the relativeOne width / 2, it should returns the relativeOne left margin, shouldn't it?
relativeOneTopMargin = ((height - relativeOneHeight) / 2); // And this, the relativeOne top margin, right?
relativeTwoLeftMargin = (int) (relativeOneLeftMargin * 1.5f);
relativeTwoTopMargin = (int) (relativeOneTopMargin * 0.78f);
relativeThreeLeftMargin = relativeOneTopMargin + relativeOneHeight;
P.S.: The relativeOne is in the middle of the screen:
relativeOne.addRule(RelativeLayout.CENTER_IN_PARENT);
I also tried to get the margins this way, but it returns 0:
relativeOne.getLeft();
relativeOne.getTop();
I really don't know why it's not in the same position in all devices. Any suggestion?
First, when using positioning in different devices, it is recommended to use DP (device-dependent points). You can see how to use them at http://developer.android.com/guide/practices/screens_support.html
When you use:
relativeOne.getLeft();
relativeOne.getTop();
You will always get 0, as, at the moment you make this call, Android did not finish drawing the parent of your layout, thus it has no information about its relative positioning. If you want to has access to this information, you have to make the call after the drawing. One way to do this is by using method post of class View:
parent.post(new Runnable() {
#Override
public void run() {
relativeOne.getLeft();
relativeOne.getTop();
}
});
In this way, you execute the call after the layout has been drawn.
I changed this code:
relativeTwoLeftMargin = (int) (relativeOneLeftMargin * 1.5f);
relativeTwoTopMargin = (int) (relativeOneTopMargin * 0.78f);
To this one:
relativeTwoLeftMargin = Math.round(relativeOneLeftMargin * 1.5f);
relativeTwoTopMargin = Math.round(relativeOneTopMargin * 0.78f);
Now it's working. I guess the int cast did not return the exactly value.

Categories

Resources