Android monospaced font size - android

I am doing a graphical code editor where I can modify constants by dragging them.
I want to highlight the commands in the code with blue rectangles such that left and right borders lay in the middle of characters, but the blue rectangles are still misaligned in some cases:
My idea is to first compute the char width and char space, and then multiply them afterwards by the position of my command in my text.
val mCodePaint = new TextPaint()
mCodePaint.setTypeface(Typeface.MONOSPACE)
mCodePaint.setAntiAlias(true)
mCodePaint.setSubpixelText(true)
mCodePaint.setColor(0xFF000000)
val dimText = new Rect()
val dimText1 = new Rect()
val dimText2 = new Rect()
final val s1 = "WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW"
final val s2 = "WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW"
// dimText1.width() = char_length * s1.length + space_between_chars*(s1.length-1)
// dimText2.width() = char_length * s2.length + space_between_chars*(s2.length-1)
def getCharWidth(): Float = {
mCodePaint.getTextBounds(s1, 0, s1.length, dimText1)
mCodePaint.getTextBounds(s2, 0, s2.length, dimText2)
(dimText2.width() * (s1.length - 1) - dimText1.width() *(s2.length - 1))/(s1.length - s2.length)
}
def getIntercharWidth(): Float = {
mCodePaint.getTextBounds(s1, 0, s1.length, dimText1)
mCodePaint.getTextBounds(s2, 0, s2.length, dimText2)
(dimText1.width * s2.length - dimText2.width * s1.length)/(s1.length - s2.length)
}
// The main function that draw the text
def drawRuleCode(canvas: Canvas, ...): Unit = {
var char_width = getCharWidth() // At run time, equals 29
var space_width = getIntercharWidth() // At run time, equals -10
for(action <- ...) {
...
val column = action.column
val length = action.length
val x1 = left_x+8 + column*char_width + (column-1)*space_width - 0.5f*space_width
val x2 = x1 + length*char_width + (length-1)*space_width + 1*space_width
rectFData.set(x1, y1, x2, y2)
canvas.drawRoundRect(rectFData, 5, 5, selectPaint)
}
for(line <- ...) {
...
canvas.drawText(s, left_x + 8, ..., mCodePaint)
}
Do you have any idea on how to overcome that small alignment problem? Sometimes it makes a huge difference, especially when the expression is long.
EDIT: I drawed the computed text bounds, and actually they are wrong. The text is slightly larger than the rectangle given by getTextBounds (violet line):

Instead of using getTextBounds, I need to pass the scale argument, because the font size does not scale linearly with the canvas:
Explanation here
var c = new Matrix()
val c_array = new Array[Float](9)
// The main function that draw the text
def drawRuleCode(canvas: Canvas, ...): Unit = {
var box_width = getBoxWidth()
canvas.getMatrix(c)
c.getValues(c_array)
val scale = c_array(Matrix.MSCALE_X) // Compute the current matrix scale
var box_width = getBoxWidth(scale)
for(action <- ...) {
...
val column = action.column
val length = action.length
val x1 = left_x+8 + column*box_width
val x2 = x1 + length*box_width
rectFData.set(x1, y1, x2, y2)
canvas.drawRoundRect(rectFData, 5, 5, selectPaint)
}
def getBoxWidth(scale: Float): Float = {
mCodePaint.setTextSize(fontSize * scale)
val result = mCodePaint.measureText(s1).toFloat / s1.length / scale
mCodePaint.setTextSize(fontSize )
result
}

Related

How to run Channel first tflite model in Android

I am able to run my custom tflite model in android but the output is totally wrong. I suspect it is due to my model needs input shape [1, 3, 640, 640] but the code makes channel last ByteBuffer. I have created tensor buffer like this TensorBuffer.createFixedSize(intArrayOf(1, 3, 640, 640), DataType.FLOAT32) but I still suspect inside the for loop, the channel is not properly set in the flat input (ByteBuffer).
I have copied this code from example where the required model shape was [1,32,32,3] (channel last). This is the reason for my doubt.
Below is my code:-
val model = YoloxPlate.newInstance(applicationContext)
val inputFeature0 = TensorBuffer.createFixedSize(intArrayOf(1, 3, 640, 640), DataType.FLOAT32)
val input = ByteBuffer.allocateDirect(640*640*3*4).order(ByteOrder.nativeOrder())
for (y in 0 until 640) {
for (x in 0 until 640) {
val px = bitmap.getPixel(x, y)
// Get channel values from the pixel value.
val r = Color.red(px)
val g = Color.green(px)
val b = Color.blue(px)
// Normalize channel values to [-1.0, 1.0]. This requirement depends on the model.
// For example, some models might require values to be normalized to the range
// [0.0, 1.0] instead.
val rf = r/ 1f
val gf = g/ 1f
val bf = b/ 1f
input.putFloat(bf)
input.putFloat(gf)
input.putFloat(rf)
}
}
inputFeature0.loadBuffer(input)
val outputs = model.process(inputFeature0)
val outputFeature0 = outputs.outputFeature0AsTensorBuffer
val flvals = outputFeature0.getFloatArray();
After using whiteboard and making and setting dim manually of the matrix, I figured it out.
It also used BGR instead of RGB as required by the model.
Working Perfectly now, here is the code (need to optimize multiple loop):-
val model = YoloxPlate.newInstance(applicationContext)
val inputFeature0 = TensorBuffer.createFixedSize(intArrayOf(1, 3, 640, 640), DataType.FLOAT32)
val input = ByteBuffer.allocateDirect(640*640*3*4).order(ByteOrder.nativeOrder())
for (y in 0 until 640) {
for (x in 0 until 640) {
val px = bitmap.getPixel(x, y)
val b = Color.blue(px)
val bf = b/ 1f
input.putFloat(bf)
}
}
for (y in 0 until 640) {
for (x in 0 until 640) {
val px = bitmap.getPixel(x, y)
val g = Color.green(px)
val gf = g/ 1f
input.putFloat(gf)
}
}
for (y in 0 until 640) {
for (x in 0 until 640) {
val px = bitmap.getPixel(x, y)
val r = Color.red(px)
val rf = r/ 1f
input.putFloat(rf)
}
}
inputFeature0.loadBuffer(input)
val outputs = model.process(inputFeature0)
val outputFeature0 = outputs.outputFeature0AsTensorBuffer
val flvals = outputFeature0.getFloatArray();

MPAndroidChart - Piechart - custom label lines

I'm trying to draw the label lines as in picture using MPAndroidChart with a pie chart. I can't figure out how to
decouple the lines from the chart
draw that little circle at the beginning of the line.
Thank you.
This is by no means easy to achieve. To decouple the lines from the chart, you can use valueLinePart1OffsetPercentage and play with line part lengths. But to get the chart to draw dots at the end of lines, you need a custom renderer. Here's one:
class CustomPieChartRenderer(pieChart: PieChart, val circleRadius: Float)
: PieChartRenderer(pieChart, pieChart.animator, pieChart.viewPortHandler) {
override fun drawValues(c: Canvas) {
super.drawValues(c)
val center = mChart.centerCircleBox
val radius = mChart.radius
var rotationAngle = mChart.rotationAngle
val drawAngles = mChart.drawAngles
val absoluteAngles = mChart.absoluteAngles
val phaseX = mAnimator.phaseX
val phaseY = mAnimator.phaseY
val roundedRadius = (radius - radius * mChart.holeRadius / 100f) / 2f
val holeRadiusPercent = mChart.holeRadius / 100f
var labelRadiusOffset = radius / 10f * 3.6f
if (mChart.isDrawHoleEnabled) {
labelRadiusOffset = (radius - radius * holeRadiusPercent) / 2f
if (!mChart.isDrawSlicesUnderHoleEnabled && mChart.isDrawRoundedSlicesEnabled) {
rotationAngle += roundedRadius * 360 / (Math.PI * 2 * radius).toFloat()
}
}
val labelRadius = radius - labelRadiusOffset
val dataSets = mChart.data.dataSets
var angle: Float
var xIndex = 0
c.save()
for (i in dataSets.indices) {
val dataSet = dataSets[i]
val sliceSpace = getSliceSpace(dataSet)
for (j in 0 until dataSet.entryCount) {
angle = if (xIndex == 0) 0f else absoluteAngles[xIndex - 1] * phaseX
val sliceAngle = drawAngles[xIndex]
val sliceSpaceMiddleAngle = sliceSpace / (Utils.FDEG2RAD * labelRadius)
angle += (sliceAngle - sliceSpaceMiddleAngle / 2f) / 2f
if (dataSet.valueLineColor != ColorTemplate.COLOR_NONE) {
val transformedAngle = rotationAngle + angle * phaseY
val sliceXBase = cos(transformedAngle * Utils.FDEG2RAD.toDouble()).toFloat()
val sliceYBase = sin(transformedAngle * Utils.FDEG2RAD.toDouble()).toFloat()
val valueLinePart1OffsetPercentage = dataSet.valueLinePart1OffsetPercentage / 100f
val line1Radius = if (mChart.isDrawHoleEnabled) {
(radius - radius * holeRadiusPercent) * valueLinePart1OffsetPercentage + radius * holeRadiusPercent
} else {
radius * valueLinePart1OffsetPercentage
}
val px = line1Radius * sliceXBase + center.x
val py = line1Radius * sliceYBase + center.y
if (dataSet.isUsingSliceColorAsValueLineColor) {
mRenderPaint.color = dataSet.getColor(j)
}
c.drawCircle(px, py, circleRadius, mRenderPaint)
}
xIndex++
}
}
MPPointF.recycleInstance(center)
c.restore()
}
}
This custom renderer extends the default pie chart renderer. I basically just copied the code from PieChartRenderer.drawValues method, converted it to Kotlin, and removed everything that wasn't needed. I only kept the logic needed to determine the position of the points at the end of lines.
I tried to reproduce the image you showed:
val chart: PieChart = view.findViewById(R.id.pie_chart)
chart.setExtraOffsets(40f, 0f, 40f, 0f)
// Custom renderer used to add dots at the end of value lines.
chart.renderer = CustomPieChartRenderer(chart, 10f)
val dataSet = PieDataSet(listOf(
PieEntry(40f),
PieEntry(10f),
PieEntry(10f),
PieEntry(15f),
PieEntry(10f),
PieEntry(5f),
PieEntry(5f),
PieEntry(5f)
), "Pie chart")
// Chart colors
val colors = listOf(
Color.parseColor("#4777c0"),
Color.parseColor("#a374c6"),
Color.parseColor("#4fb3e8"),
Color.parseColor("#99cf43"),
Color.parseColor("#fdc135"),
Color.parseColor("#fd9a47"),
Color.parseColor("#eb6e7a"),
Color.parseColor("#6785c2"))
dataSet.colors = colors
dataSet.setValueTextColors(colors)
// Value lines
dataSet.valueLinePart1Length = 0.6f
dataSet.valueLinePart2Length = 0.3f
dataSet.valueLineWidth = 2f
dataSet.valueLinePart1OffsetPercentage = 115f // Line starts outside of chart
dataSet.isUsingSliceColorAsValueLineColor = true
// Value text appearance
dataSet.yValuePosition = PieDataSet.ValuePosition.OUTSIDE_SLICE
dataSet.valueTextSize = 16f
dataSet.valueTypeface = Typeface.DEFAULT_BOLD
// Value formatting
dataSet.valueFormatter = object : ValueFormatter() {
private val formatter = NumberFormat.getPercentInstance()
override fun getFormattedValue(value: Float) =
formatter.format(value / 100f)
}
chart.setUsePercentValues(true)
dataSet.selectionShift = 3f
// Hole
chart.isDrawHoleEnabled = true
chart.holeRadius = 50f
// Center text
chart.setDrawCenterText(true)
chart.setCenterTextSize(20f)
chart.setCenterTextTypeface(Typeface.DEFAULT_BOLD)
chart.setCenterTextColor(Color.parseColor("#222222"))
chart.centerText = "Center\ntext"
// Disable legend & description
chart.legend.isEnabled = false
chart.description = null
chart.data = PieData(dataSet)
Again, not very straightforward. I hope you like Kotlin! You can move most of that configuration code to a subclass if you need it often. Here's the result:
I'm not a MPAndroidChart expert. In fact, I've used it only once, and that was 2 years ago. But if you do your research, you can find a solution most of the time. Luckily, MPAndroidChart is a very customizable.

Android Path Not Closing, I can't fill with color

I have a custom squircle android view. I am drawing a Path but and I cannot fill the path with color.
StackOverflow is asking me to provide more detail, but I don't think I can explain this better. All I need is to fill the path with code. If you need to see the whole class let me know.
init {
val typedValue = TypedValue()
val theme = context!!.theme
theme.resolveAttribute(R.attr.colorAccentTheme, typedValue, true)
paint.color = typedValue.data
paint.strokeWidth = 6f
paint.style = Paint.Style.FILL_AND_STROKE
paint.isAntiAlias = true
shapePadding = 10f
}
open fun onLayoutInit() {
val hW = (this.measuredW / 2) - shapePadding
val hH = (this.measuredH / 2) - shapePadding
/*
Returns a series of Vectors along the path
of the squircle
*/
points = Array(360) { i ->
val angle = toRadians(i.toDouble())
val x = pow(abs(cos(angle)), corners) * hW * sgn(cos(angle))
val y = pow(abs(sin(angle)), corners) * hH * sgn(sin(angle))
Pair(x.toFloat(), y.toFloat())
}
/*
Match the path to the points
*/
for (i in 0..points.size - 2) {
val p1 = points[i]
val p2 = points[i + 1]
path.moveTo(p1.first, p1.second)
path.lineTo(p2.first, p2.second)
}
/*
Finish closing the path's points
*/
val fst = points[0]
val lst = points[points.size - 1]
path.moveTo(lst.first, lst.second)
path.lineTo(fst.first, fst.second)
path.fillType = Path.FillType.EVEN_ODD
path.close()
postInvalidate()
}
override fun onDraw(canvas: Canvas?) {
canvas?.save()
canvas?.translate(measuredW / 2, measuredH / 2)
canvas?.drawPath(path, paint)
canvas?.restore()
super.onDraw(canvas)
}
}
The path stroke works fine, but I can't fill it with color.

How do launchers change the shape of an adaptive icon, including removal of background?

Background
Starting from Android O, apps can have adaptive icons, which are 2 layers of drawables: foreground and a background. The background is a mask that gets to be a shape of the launcher/user's choice, while the OS has a default shape for it too.
Here's an example of what Nova Launcher allows to do:
As you can see, it allows not only to choose which shape to use, but also avoid a shape at all (in "prefer legacy icons").
Here are some links about it:
https://www.youtube.com/watch?v=5MHFYfXno9c
https://medium.com/#ianhlake/vectordrawable-adaptive-icons-3fed3d3205b5
The problem
While I know how to create a AdaptiveIconDrawable instance, and I'm aware of the wizard that helps creating one for the current app, I don't get how, given an AdaptiveIconDrawable instance, launchers change the shape.
Not only that, but I remember I saw a launcher or two that allows to not have any shape.
Sadly I can't find any information about this part, maybe because this is a relatively very new feature. There isn't even a keyword for it here on StackOverflow.
What I've tried
I tried reading about adaptive icons, but couldn't find a reference to the receiver side.
I know it has the 2 drawables within it:
https://developer.android.com/reference/android/graphics/drawable/AdaptiveIconDrawable.html#getBackground()
https://developer.android.com/reference/android/graphics/drawable/AdaptiveIconDrawable.html#getForeground()
I know, at least, how to get an AdaptiveIconDrawable instance out of a third party app (assuming it has one) :
PackageManager pm = context.getPackageManager();
Intent launchIntentForPackage = pm.getLaunchIntentForPackage(packageName);
String fullPathToActivity = launchIntentForPackage.getComponent().getClassName();
ActivityInfo activityInfo = pm.getActivityInfo(new ComponentName(packageName, fullPathToActivity), 0);
int iconRes = activityInfo.icon;
Drawable drawable = pm.getDrawable(packageName, iconRes, activityInfo.applicationInfo); // will be AdaptiveIconDrawable, if the app has it
The questions
Given a AdaptiveIconDrawable instance, how do you shape it, to be of a circular shape, rectangle, rounded rectangle, tear, and so on?
How do I remove the shape and still have a valid size of the icon (using its foreground drawable in it) ? The official size of an app icon for launchers is 48 dp, while the official ones for AdaptiveIconDrawable inner drawables are 72dp (foreground), 108dp (background). I guess this would mean taking the foreground drawable, resize it somehow, and convert to a bitmap.
In which case exactly is it useful to use IconCompat.createWithAdaptiveBitmap() ? It was written that "If you’re building a dynamic shortcut using a Bitmap, you might find the Support Library 26.0.0-beta2’s IconCompat.createWithAdaptiveBitmap() useful in ensuring that your Bitmap is masked correctly to match other adaptive icons." , but I don't get which cases it's useful for.
EDIT: In order to create a bitmap out of the foreground part of the adaptive icon, while resizing to a proper size, I think this could be a good solution:
val foregroundBitmap = convertDrawableToBitmap(drawable.foreground)
val targetSize = convertDpToPixels(this, ...).toInt()
val scaledBitmap = ThumbnailUtils.extractThumbnail(foregroundBitmap, targetSize, targetSize, ThumbnailUtils.OPTIONS_RECYCLE_INPUT)
fun convertDrawableToBitmap(drawable: Drawable?): Bitmap? {
if (drawable == null)
return null
if (drawable is BitmapDrawable) {
return drawable.bitmap
}
val bounds = drawable.bounds
val width = if (!bounds.isEmpty) bounds.width() else drawable.intrinsicWidth
val height = if (!bounds.isEmpty) bounds.height() else drawable.intrinsicHeight
val bitmap = Bitmap.createBitmap(if (width <= 0) 1 else width, if (height <= 0) 1 else height,
Bitmap.Config.ARGB_8888)
val canvas = Canvas(bitmap)
drawable.setBounds(0, 0, canvas.width, canvas.height)
drawable.draw(canvas)
drawable.bounds = bounds;
return bitmap
}
fun convertDpToPixels(context: Context, dp: Float): Float = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, context.resources.displayMetrics)
Might be able to avoid having 2 bitmaps at the same time, but this is ok I think.
About the creation of a shaped drawable of various types, I'm still not sure how to do it. Only solution I've seen by the answers below is of using a rounded rectangle or a circle, but there are other shapes (for example the tear) that can come to mind.
EDIT:
I was told as some point by Google (here) that I should use AdaptiveIconDrawable.getIconMask(), but I wasn't given any further information. However, I've found a nice article about this here.
I don't get how, given an AdaptiveIconDrawable instance, launchers change the shape.
Launchers are just apps, so they simply draw the background in the shape they want (or the user selected) and then draw the foreground on top.
I don't have a sample project of my own, but Nick Butcher made a great sample project and series of blog posts: AdaptiveIconPlayground.
Given a AdaptiveIconDrawable instance, how do you shape it, to be of a circular shape, rectangle, rounded rectangle, tear, and so on?
The simplest way is to rasterize the drawable and draw the bitmap using a shader like it is done in Nick's AdaptiveIconView:
private val backgroundPaint = Paint(Paint.ANTI_ALIAS_FLAG)
private val background: Bitmap
// ...
background = Bitmap.createBitmap(layerSize, layerSize, Bitmap.Config.ARGB_8888)
backgroundPaint.shader = BitmapShader(background, CLAMP, CLAMP)
// < rasterize drawable onto `background` >
// draw desired shape(s)
canvas.drawRoundRect(0f, 0f, iconSize.toFloat(), iconSize.toFloat(),
cornerRadius, cornerRadius, backgroundPaint)
How do I remove the shape and still have a valid size of the icon (using its foreground drawable in it) ? The official size of an app icon for launchers is 48 dp, while the official ones for AdaptiveIconDrawable inner drawables are 72dp (foreground), 108dp (background). I guess this would mean taking the foreground drawable, resize it somehow, and convert to a bitmap.
If you don't want a background, just don't draw it. You're in full control. The size does not really matter, because you usually know how big your icons should be drawn. The documentation states that foreground and background should be 108dp, so you can simply downscale your drawing. If foreground/background use vector graphics, then size really does not matter, as you can just draw them however big you like.
If you rasterize the foreground, then you can do custom drawing as seen above, or choose Canvas#drawBitmap(...), which also offers multiple options to draw a Bitmap, including to pass in a transformation matrix, or simply some bounds.
If you don't rasterize your drawable you can also use drawable.setBounds(x1, y1, x2, y2), where you can set the bounds on where the drawable should draw itself. This should also work.
In which case exactly is it useful to use IconCompat.createWithAdaptiveBitmap() ? It was written that "If you’re building a dynamic shortcut using a Bitmap, you might find the Support Library 26.0.0-beta2’s IconCompat.createWithAdaptiveBitmap() useful in ensuring that your Bitmap is masked correctly to match other adaptive icons." , but I don't get which cases it's useful for.
ShortCutInfo.Builder has a setIcon(Icon icon) method where you need to pass it in. (And the same applies for the compat versions)
It seems that Icon is used to have control over the kind of Bitmap that gets passed in as an icon. Right now I could not find any other usage for Icon. I don't think that you would use this when creating a launcher.
More information reflecting the last comment
Do you wrap the AdaptiveIconDrawable class with your own drawable? I just want to convert it somehow to something I can use, to both an ImageView and a Bitmap, and I wish to control the shape, using all shapes I've shown on the screenshot above. How would I do it?
If you follow the links above you can see a custom AdaptiveIconView that draws the AdaptiveIconDrawable, so doing a custom view is definitely an option, but everything mentioned can be moved just as easily into a custom Drawable, which you then could also use with a basic ImageView.
You can achieve the various different backgrounds by using the methods available on Canvas along with a BitmapShader as shown above, e.g. additionally to drawRoundRect we would have
canvas.drawCircle(centerX, centerY, radius, backgroundPaint) // circle
canvas.drawRect(0f, 0f, width, height, backgroundPaint) // rect
canvas.drawPath(path, backgroundPaint) // more complex shapes
To switch between background shapes you could use anything from if/else, over composition, to inheritance, and just draw the shape you like.
OK I got something to work, but for some reason the inner icon seems smaller than what's done with the AdaptiveIconDrawable. Also for some reason, on the way, it affected the original AdaptiveIconDrawable (even if I used mutate on any drawable I used) so I had to create a new one to demonstrate the original vs new one. Another small annoyance is that to create the masked bitmap, I had to have 2 Bitmap instances (drawable converted to one, and needed an output too).
I wonder if it's possible to convert the drawable directly to a Bitmap/Drawable that has the given shape, so I asked about this here.
So, suppose you have a Path instance. You can get one from the AdaptiveIconDrawable.getIconMask function (which is the one of the system), or you can create one yourself, such as the one used here (repository here) or here.
If anyone knows how to solve those issues I've mentioned above (smaller foreground and affects original drawable, and maybe a better conversion), please let me know. For now, you can either use this solution, or use a library like here.
Now, suppose you get the AdaptiveIconDrawable instance, and you want to shape it in the same shape as of the Path instance.
So, what you can do is something like what's below (PathUtils is converted to Kotlin from either repositories) , and the result:
MainActivity.kt
class MainActivity : AppCompatActivity(R.layout.activity_main) {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val appIcon = applicationInfo.loadIcon(packageManager)
originalIconImageView.setImageDrawable(applicationInfo.loadIcon(packageManager))
if (appIcon is AdaptiveIconDrawable) {
val iconMask = getPath(PATH_SQUIRCLE)
val maskedBitmap = getMaskedBitmap(appIcon.background, iconMask)
val foreground = appIcon.foreground
val layerDrawable = LayerDrawable(arrayOf(BitmapDrawable(resources, maskedBitmap), foreground))
maskedImageView.setImageDrawable(layerDrawable)
}
}
companion object {
const val PATH_CIRCLE = 0
const val PATH_SQUIRCLE = 1
const val PATH_ROUNDED_SQUARE = 2
const val PATH_SQUARE = 3
const val PATH_TEARDROP = 4
fun resizePath(path: Path, width: Float, height: Float): Path {
val bounds = RectF(0f, 0f, width, height)
val resizedPath = Path(path)
val src = RectF()
resizedPath.computeBounds(src, true)
val resizeMatrix = Matrix()
resizeMatrix.setRectToRect(src, bounds, Matrix.ScaleToFit.CENTER)
resizedPath.transform(resizeMatrix)
return resizedPath
}
fun getMaskedBitmap(src: Bitmap, path: Path, resizePathToMatchBitmap: Boolean = true): Bitmap {
val pathToUse = if (resizePathToMatchBitmap) resizePath(path, src.width.toFloat(), src.height.toFloat()) else path
val output = Bitmap.createBitmap(src.width, src.height, Bitmap.Config.ARGB_8888)
val canvas = Canvas(output)
val paint = Paint(Paint.ANTI_ALIAS_FLAG)
paint.color = 0XFF000000.toInt()
canvas.drawPath(pathToUse, paint)
paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_IN)
canvas.drawBitmap(src, 0f, 0f, paint)
return output
}
fun getMaskedBitmap(drawable: Drawable, path: Path, resizePathToMatchBitmap: Boolean = true): Bitmap = getMaskedBitmap(drawable.toBitmap(), path, resizePathToMatchBitmap)
fun getPath(pathType: Int): Path {
val path = Path()
val pathSize = Rect(0, 0, 50, 50)
when (pathType) {
PATH_CIRCLE -> {
path.arcTo(RectF(pathSize), 0f, 359f)
path.close()
}
PATH_SQUIRCLE -> path.set(PathUtils.createPathFromPathData("M 50,0 C 10,0 0,10 0,50 C 0,90 10,100 50,100 C 90,100 100,90 100,50 C 100,10 90,0 50,0 Z"))
PATH_ROUNDED_SQUARE -> path.set(PathUtils.createPathFromPathData("M 50,0 L 70,0 A 30,30,0,0 1 100,30 L 100,70 A 30,30,0,0 1 70,100 L 30,100 A 30,30,0,0 1 0,70 L 0,30 A 30,30,0,0 1 30,0 z"))
PATH_SQUARE -> {
path.lineTo(0f, 50f)
path.lineTo(50f, 50f)
path.lineTo(50f, 0f)
path.lineTo(0f, 0f)
path.close()
}
PATH_TEARDROP -> path.set(PathUtils.createPathFromPathData("M 50,0 A 50,50,0,0 1 100,50 L 100,85 A 15,15,0,0 1 85,100 L 50,100 A 50,50,0,0 1 50,0 z"))
}
return path
}
}
}
activity_main.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical"
tools:context=".MainActivity">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Original:" />
<ImageView
android:id="#+id/originalIconImageView"
android:layout_width="200dp"
android:layout_height="200dp"
android:layout_marginTop="16dp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="Masked:" />
<ImageView
android:id="#+id/maskedImageView"
android:layout_width="200dp"
android:layout_height="200dp"
android:layout_marginTop="16dp" />
</LinearLayout>
PathUtils.kt
object PathUtils {
/**
* #param pathData The string representing a path, the same as "d" string in svg file.
* #return the generated Path object.
*/
fun createPathFromPathData(pathData: String): Path {
val path = Path()
val nodes = createNodesFromPathData(pathData)
PathDataNode.nodesToPath(nodes, path)
return path
}
/**
* #param pathData The string representing a path, the same as "d" string in svg file.
* #return an array of the PathDataNode.
*/
fun createNodesFromPathData(pathData: String): Array<PathDataNode> {
var start = 0
var end = 1
val list = ArrayList<PathDataNode>()
while (end < pathData.length) {
end = nextStart(pathData, end)
val s = pathData.substring(start, end)
val `val` = getFloats(s)
addNode(list, s[0], `val`)
start = end
end++
}
if (end - start == 1 && start < pathData.length) {
addNode(list, pathData[start], FloatArray(0))
}
return list.toTypedArray()
}
private fun nextStart(s: String, inputEnd: Int): Int {
var end = inputEnd
var c: Char
while (end < s.length) {
c = s[end]
if ((c - 'A') * (c - 'Z') <= 0 || (c - 'a') * (c - 'z') <= 0) return end
end++
}
return end
}
private fun addNode(list: ArrayList<PathDataNode>, cmd: Char, `val`: FloatArray) {
list.add(PathDataNode(cmd, `val`))
}
/**
* Parse the floats in the string.
* This is an optimized version of parseFloat(s.split(",|\\s"));
*
* #param s the string containing a command and list of floats
* #return array of floats
*/
#Throws(NumberFormatException::class)
private fun getFloats(s: String): FloatArray {
if (s[0] == 'z' || s[0] == 'Z')
return FloatArray(0)
val tmp = FloatArray(s.length)
var count = 0
var pos = 1
var end: Int
while (extract(s, pos).also { end = it } >= 0) {
if (pos < end) tmp[count++] = s.substring(pos, end).toFloat()
pos = end + 1
}
// handle the final float if there is one
if (pos < s.length) tmp[count++] = s.substring(pos).toFloat()
return tmp.copyOf(count)
}
/**
* Calculate the position of the next comma or space
*
* #param s the string to search
* #param start the position to start searching
* #return the position of the next comma or space or -1 if none found
*/
private fun extract(s: String, start: Int): Int {
val space = s.indexOf(' ', start)
val comma = s.indexOf(',', start)
if (space == -1) return comma
return if (comma == -1) space else Math.min(comma, space)
}
class PathDataNode(private val type: Char, private var params: FloatArray) {
#Suppress("unused")
constructor(n: PathDataNode) : this(n.type, n.params.copyOf(n.params.size))
companion object {
fun nodesToPath(node: Array<PathDataNode>, path: Path) {
val current = FloatArray(4)
var previousCommand = 'm'
for (pathDataNode in node) {
addCommand(path, current, previousCommand, pathDataNode.type, pathDataNode.params)
previousCommand = pathDataNode.type
}
}
private fun addCommand(path: Path, current: FloatArray, inputPreviousCmd: Char, cmd: Char, floats: FloatArray) {
var previousCmd = inputPreviousCmd
var incr = 2
var currentX = current[0]
var currentY = current[1]
var ctrlPointX = current[2]
var ctrlPointY = current[3]
var reflectiveCtrlPointX: Float
var reflectiveCtrlPointY: Float
when (cmd) {
'z', 'Z' -> {
path.close()
return
}
'm', 'M', 'l', 'L', 't', 'T' -> incr = 2
'h', 'H', 'v', 'V' -> incr = 1
'c', 'C' -> incr = 6
's', 'S', 'q', 'Q' -> incr = 4
'a', 'A' -> incr = 7
}
var k = 0
while (k < floats.size) {
when (cmd) {
'm' -> {
path.rMoveTo(floats[k], floats[k + 1])
currentX += floats[k]
currentY += floats[k + 1]
}
'M' -> {
path.moveTo(floats[k], floats[k + 1])
currentX = floats[k]
currentY = floats[k + 1]
}
'l' -> {
path.rLineTo(floats[k], floats[k + 1])
currentX += floats[k]
currentY += floats[k + 1]
}
'L' -> {
path.lineTo(floats[k], floats[k + 1])
currentX = floats[k]
currentY = floats[k + 1]
}
'h' -> {
path.rLineTo(floats[k], 0f)
currentX += floats[k]
}
'H' -> {
path.lineTo(floats[k], currentY)
currentX = floats[k]
}
'v' -> {
path.rLineTo(0f, floats[k])
currentY += floats[k]
}
'V' -> {
path.lineTo(currentX, floats[k])
currentY = floats[k]
}
'c' -> {
path.rCubicTo(floats[k], floats[k + 1], floats[k + 2], floats[k + 3], floats[k + 4], floats[k + 5])
ctrlPointX = currentX + floats[k + 2]
ctrlPointY = currentY + floats[k + 3]
currentX += floats[k + 4]
currentY += floats[k + 5]
}
'C' -> {
path.cubicTo(floats[k], floats[k + 1], floats[k + 2], floats[k + 3],
floats[k + 4], floats[k + 5])
currentX = floats[k + 4]
currentY = floats[k + 5]
ctrlPointX = floats[k + 2]
ctrlPointY = floats[k + 3]
}
's' -> {
reflectiveCtrlPointX = 0f
reflectiveCtrlPointY = 0f
if (previousCmd == 'c' || previousCmd == 's' || previousCmd == 'C' || previousCmd == 'S') {
reflectiveCtrlPointX = currentX - ctrlPointX
reflectiveCtrlPointY = currentY - ctrlPointY
}
path.rCubicTo(reflectiveCtrlPointX, reflectiveCtrlPointY, floats[k], floats[k + 1], floats[k + 2], floats[k + 3])
ctrlPointX = currentX + floats[k]
ctrlPointY = currentY + floats[k + 1]
currentX += floats[k + 2]
currentY += floats[k + 3]
}
'S' -> {
reflectiveCtrlPointX = currentX
reflectiveCtrlPointY = currentY
if (previousCmd == 'c' || previousCmd == 's' || previousCmd == 'C' || previousCmd == 'S') {
reflectiveCtrlPointX = 2 * currentX - ctrlPointX
reflectiveCtrlPointY = 2 * currentY - ctrlPointY
}
path.cubicTo(reflectiveCtrlPointX, reflectiveCtrlPointY, floats[k], floats[k + 1], floats[k + 2], floats[k + 3])
ctrlPointX = floats[k]
ctrlPointY = floats[k + 1]
currentX = floats[k + 2]
currentY = floats[k + 3]
}
'q' -> {
path.rQuadTo(floats[k], floats[k + 1], floats[k + 2], floats[k + 3])
ctrlPointX = currentX + floats[k]
ctrlPointY = currentY + floats[k + 1]
currentX += floats[k + 2]
currentY += floats[k + 3]
}
'Q' -> {
path.quadTo(floats[k], floats[k + 1], floats[k + 2], floats[k + 3])
ctrlPointX = floats[k]
ctrlPointY = floats[k + 1]
currentX = floats[k + 2]
currentY = floats[k + 3]
}
't' -> {
reflectiveCtrlPointX = 0f
reflectiveCtrlPointY = 0f
if (previousCmd == 'q' || previousCmd == 't' || previousCmd == 'Q' || previousCmd == 'T') {
reflectiveCtrlPointX = currentX - ctrlPointX
reflectiveCtrlPointY = currentY - ctrlPointY
}
path.rQuadTo(reflectiveCtrlPointX, reflectiveCtrlPointY,
floats[k], floats[k + 1])
ctrlPointX = currentX + reflectiveCtrlPointX
ctrlPointY = currentY + reflectiveCtrlPointY
currentX += floats[k]
currentY += floats[k + 1]
}
'T' -> {
reflectiveCtrlPointX = currentX
reflectiveCtrlPointY = currentY
if (previousCmd == 'q' || previousCmd == 't' || previousCmd == 'Q' || previousCmd == 'T') {
reflectiveCtrlPointX = 2 * currentX - ctrlPointX
reflectiveCtrlPointY = 2 * currentY - ctrlPointY
}
path.quadTo(reflectiveCtrlPointX, reflectiveCtrlPointY, floats[k], floats[k + 1])
ctrlPointX = reflectiveCtrlPointX
ctrlPointY = reflectiveCtrlPointY
currentX = floats[k]
currentY = floats[k + 1]
}
'a' -> {
// (rx ry x-axis-rotation large-arc-flag sweep-flag x y)
drawArc(path, currentX, currentY, floats[k + 5] + currentX, floats[k + 6] + currentY, floats[k],
floats[k + 1], floats[k + 2], floats[k + 3] != 0f, floats[k + 4] != 0f)
currentX += floats[k + 5]
currentY += floats[k + 6]
ctrlPointX = currentX
ctrlPointY = currentY
}
'A' -> {
drawArc(path, currentX, currentY, floats[k + 5], floats[k + 6], floats[k], floats[k + 1], floats[k + 2],
floats[k + 3] != 0f, floats[k + 4] != 0f)
currentX = floats[k + 5]
currentY = floats[k + 6]
ctrlPointX = currentX
ctrlPointY = currentY
}
}
previousCmd = cmd
k += incr
}
current[0] = currentX
current[1] = currentY
current[2] = ctrlPointX
current[3] = ctrlPointY
}
private fun drawArc(p: Path, x0: Float, y0: Float, x1: Float, y1: Float, a: Float, b: Float, theta: Float, isMoreThanHalf: Boolean, isPositiveArc: Boolean) {
/* Convert rotation angle from degrees to radians */
val thetaD = Math.toRadians(theta.toDouble())
/* Pre-compute rotation matrix entries */
val cosTheta = Math.cos(thetaD)
val sinTheta = Math.sin(thetaD)
/* Transform (x0, y0) and (x1, y1) into unit space */
/* using (inverse) rotation, followed by (inverse) scale */
val x0p = (x0 * cosTheta + y0 * sinTheta) / a
val y0p = (-x0 * sinTheta + y0 * cosTheta) / b
val x1p = (x1 * cosTheta + y1 * sinTheta) / a
val y1p = (-x1 * sinTheta + y1 * cosTheta) / b
/* Compute differences and averages */
val dx = x0p - x1p
val dy = y0p - y1p
val xm = (x0p + x1p) / 2
val ym = (y0p + y1p) / 2
/* Solve for intersecting unit circles */
val dsq = dx * dx + dy * dy
if (dsq == 0.0) return /* Points are coincident */
val disc = 1.0 / dsq - 1.0 / 4.0
if (disc < 0.0) {
val adjust = (Math.sqrt(dsq) / 1.99999).toFloat()
drawArc(p, x0, y0, x1, y1, a * adjust, b * adjust, theta, isMoreThanHalf, isPositiveArc)
return /* Points are too far apart */
}
val s = Math.sqrt(disc)
val sdx = s * dx
val sdy = s * dy
var cx: Double
var cy: Double
if (isMoreThanHalf == isPositiveArc) {
cx = xm - sdy
cy = ym + sdx
} else {
cx = xm + sdy
cy = ym - sdx
}
val eta0 = Math.atan2(y0p - cy, x0p - cx)
val eta1 = Math.atan2(y1p - cy, x1p - cx)
var sweep = eta1 - eta0
if (isPositiveArc != sweep >= 0) {
if (sweep > 0) {
sweep -= 2 * Math.PI
} else {
sweep += 2 * Math.PI
}
}
cx *= a.toDouble()
cy *= b.toDouble()
val tcx = cx
cx = cx * cosTheta - cy * sinTheta
cy = tcx * sinTheta + cy * cosTheta
arcToBezier(p, cx, cy, a.toDouble(), b.toDouble(), x0.toDouble(), y0.toDouble(), thetaD, eta0, sweep)
}
/**
* Converts an arc to cubic Bezier segments and records them in p.
*
* #param p The target for the cubic Bezier segments
* #param cx The x coordinate center of the ellipse
* #param cy The y coordinate center of the ellipse
* #param a The radius of the ellipse in the horizontal direction
* #param b The radius of the ellipse in the vertical direction
* #param inputE1x E(eta1) x coordinate of the starting point of the arc
* #param inputE1y E(eta2) y coordinate of the starting point of the arc
* #param theta The angle that the ellipse bounding rectangle makes with horizontal plane
* #param start The start angle of the arc on the ellipse
* #param sweep The angle (positive or negative) of the sweep of the arc on the ellipse
*/
private fun arcToBezier(p: Path, cx: Double, cy: Double, a: Double, b: Double, inputE1x: Double, inputE1y: Double, theta: Double, start: Double, sweep: Double) {
// Taken from equations at: http://spaceroots.org/documents/ellipse/node8.html
// and http://www.spaceroots.org/documents/ellipse/node22.html
// Maximum of 45 degrees per cubic Bezier segment
var e1x = inputE1x
var e1y = inputE1y
val numSegments = Math.abs(Math.ceil(sweep * 4 / Math.PI).toInt())
var eta1 = start
val cosTheta = Math.cos(theta)
val sinTheta = Math.sin(theta)
val cosEta1 = Math.cos(eta1)
val sinEta1 = Math.sin(eta1)
var ep1x = -a * cosTheta * sinEta1 - b * sinTheta * cosEta1
var ep1y = -a * sinTheta * sinEta1 + b * cosTheta * cosEta1
val anglePerSegment = sweep / numSegments
for (i in 0 until numSegments) {
val eta2 = eta1 + anglePerSegment
val sinEta2 = Math.sin(eta2)
val cosEta2 = Math.cos(eta2)
val e2x = cx + a * cosTheta * cosEta2 - b * sinTheta * sinEta2
val e2y = cy + a * sinTheta * cosEta2 + b * cosTheta * sinEta2
val ep2x = -a * cosTheta * sinEta2 - b * sinTheta * cosEta2
val ep2y = -a * sinTheta * sinEta2 + b * cosTheta * cosEta2
val tanDiff2 = Math.tan((eta2 - eta1) / 2)
val alpha = Math.sin(eta2 - eta1) * (Math.sqrt(4 + 3 * tanDiff2 * tanDiff2) - 1) / 3
val q1x = e1x + alpha * ep1x
val q1y = e1y + alpha * ep1y
val q2x = e2x - alpha * ep2x
val q2y = e2y - alpha * ep2y
p.cubicTo(q1x.toFloat(), q1y.toFloat(), q2x.toFloat(), q2y.toFloat(), e2x.toFloat(), e2y.toFloat())
eta1 = eta2
e1x = e2x
e1y = e2y
ep1x = ep2x
ep1y = ep2y
}
}
}
}
}
I know two ways to build a custom shaped icon from an AdaptiveIconDrawable. I however think that Google should make a public AdaptiveIconDrawable.setMask(Path path) method:
First way (pretty same way as AOSP code):
public Bitmap createBitmap(#NonNull AdaptiveIconDrawable drawable, #NonNull Path path, int outputSize) {
// make the drawable match the output size and store its bounds to restore later
final Rect originalBounds = drawable.getBounds();
drawable.setBounds(0, 0, outputSize, outputSize);
// rasterize drawable
final Bitmap outputBitmap = Bitmap.createBitmap(outputSize, outputSize, Bitmap.Config.ARGB_8888);
final Canvas tmpCanvas = new Canvas(maskBitmap);
drawable.getBackground().draw(tmpCanvas);
drawable.getForeground().draw(tmpCanvas);
// build a paint with shader composed by the rasterized AdaptiveIconDrawable
final BitmapShader shader = new BitmapShader(outputBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG |
Paint.FILTER_BITMAP_FLAG);
paint.setShader(shader);
// draw the shader with custom path (shape)
tmpCanvas.drawPath(path, paint);
// restore drawable original bounds
drawable.setBounds(originalBounds);
return outputBitmap;
}
Second way (the one I like most, because it allows to cache the mask bitmap in case of need using multiple times, avoiding Bitmap, Canvas, BitmapShader, and Paint re-allocation). If you don't understand, make sure you check this link out:
#Nullable private Bitmap mMaskBitmap;
#Nullable private Paint mClearPaint;
#NonNull Canvas mCanvas = new Canvas();
#Nullable Path mCustomShape; // your choice
#Nullable Rect mOldBounds;
public Bitmap createBitmap(#NonNull AdaptiveIconDrawable drawable, int outputSize) {
final Bitmap outputBitmap = Bitmap.createBitmap(outputSize, outputSize, Bitmap.Config.ARGB_8888);
mCanvas.setBitmap(outputBitmap);
// rasterize the AdaptiveIconDrawable
mOldBounds = drawable.getBounds();
drawable.setBounds(0, 0, outputSize, outputSize);
drawable.getBackground().draw(mCanvas);
drawable.getForeground().draw(mCanvas);
// finally mask the bitmap, generating the desired output shape
// this clears all the pixels of the rasterized AdaptiveIconDrawable which
// fall below the maskBitmap BLACK pixels
final Bitmap maskBitmap = getMaskBitmap(mCustomShape, outputSize);
mCanvas.drawBitmap(maskBitmap, 0, 0, mClearPaint);
// restore original drawable bounds
drawable.setBounds(mOldBounds);
return outputBitmap;
}
// results a bitmap with the mask of the #path shape
private Bitmap getMaskBitmap(#Nullable Path path, int iconSize) {
if (mMaskBitmap != null && mMaskBitmap.getWidth() == iconSize && mMaskBitmap.getHeight() == iconSize)
// quick return if already cached AND size-compatible
return mMaskBitmap;
// just create a plain, black bitmap with the same size of AdaptiveIconDrawable
mMaskBitmap = Bitmap.createBitmap(iconSize, iconSize, Bitmap.Config.ALPHA_8);
mMaskBitmap.eraseColor(Color.BLACK);
final Canvas tmpCanvas = new Canvas(mMaskBitmap);
// clear the pixels inside the shape (those where the icon will be visible)
mClearPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
mClearPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT));
if (path != null)
// if path is null, the output adaptive icon will not be masked (square, full size)
tmpCanvas.drawPath(path, mClearPaint);
return mMaskBitmap;
}
I prefer the second way, but the best one depends on the usage. If only one icon is shaped, then the first one would do the job. However, for multiple icons, the second one is better to go. Share your thoughts
Launchers have much less restrictions than applications, so they may use other approaches, but one solution has been nicely showcased in Nick Butcher's Adaptive Icon Playground.
The class you're probably interested in is the Adaptive Icon View which renders adapted versions of the icon by creating a raster of each layer with the background as a canvas bitmap and then drawing those layers as rounded rectangles to implement the clipping.
The linked repo will be much more informative and includes examples of how to transform the layer for movement effects etc., but here is the basic pseudo-code for 'adapting an icon' in an image view:
setIcon() {
//erase canvas first...
canvas.setBitmap(background)
drawable.setBounds(0, 0, layerSize, layerSize)
drawable.draw(canvas)
}
onDraw() {
//draw shadow first if needed...
canvas.drawRoundRect(..., cornerRadius, backgroundPaint)
canvas.drawRoundRect(..., cornerRadius, foregroundPaint)
}
I made a custom ImageView that can have a path set to clip the background/drawable and apply the proper shadow via a custom outline provider, which includes support for reading the system preference (as confirmed on my Pixel 4/emulators, changing system icon shape is propagated to my app.)
View:
import android.content.Context
import android.graphics.*
import android.graphics.drawable.Drawable
import android.os.Build
import android.util.AttributeSet
import android.view.View
import android.view.ViewOutlineProvider
import androidx.appcompat.widget.AppCompatImageView
open class AdaptiveImageView #JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : AppCompatImageView(context, attrs, defStyleAttr) {
// Reusable to reduce object allocation
private val resizeRect = RectF()
private val srcResizeRect = RectF()
private val resizeMatrix = Matrix()
private val adaptivePathPreference = Path()
private val adaptivePathResized = Path()
private var backgroundDelegate: Drawable? = null
// Paint to clear area outside adaptive path
private val clearPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
xfermode = PorterDuffXfermode(PorterDuff.Mode.CLEAR)
}
init {
// Use the adaptive path as an outline provider
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
outlineProvider = object : ViewOutlineProvider() {
override fun getOutline(view: View, outline: Outline) {
outline.setConvexPath(adaptivePathResized)
}
}
}
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
updatePathBounds()
}
// We use saveLayer/clear rather than clipPath so we get anti-aliasing
override fun onDraw(canvas: Canvas) {
val count = canvas.saveLayer(0f, 0f, width.toFloat(), height.toFloat(), null)
backgroundDelegate?.draw(canvas)
super.onDraw(canvas)
canvas.drawPath(adaptivePathResized, clearPaint)
canvas.restoreToCount(count)
}
// Background doesn't play nice with our clipping, so hold drawable and null out so
// we can handle ourselves later.
override fun setBackground(background: Drawable?) {
backgroundDelegate = background?.apply {
if (isStateful) state = drawableState
}
if (isLaidOut) updatePathBounds()
// Null out so noone else tries to draw it (incorrectly)
super.setBackground(null)
}
override fun drawableStateChanged() {
super.drawableStateChanged()
backgroundDelegate?.apply {
if (isStateful) state = drawableState
}
}
fun setAdaptivePath(path: Path?) {
path?.let { adaptivePathPreference.set(it) } ?: adaptivePathPreference.reset()
updatePathBounds()
}
private fun updatePathBounds() {
resizePath(
left = paddingLeft.toFloat(),
top = paddingTop.toFloat(),
right = width - paddingRight.toFloat(),
bottom = height - paddingBottom.toFloat()
)
backgroundDelegate?.apply {
setBounds(
paddingLeft,
paddingTop,
width,
height
)
}
invalidate()
invalidateOutline()
}
// No object allocations
private fun resizePath(left: Float, top: Float, right: Float, bottom: Float) {
resizeRect.set(left, top, right, bottom)
adaptivePathResized.set(adaptivePathPreference)
srcResizeRect.set(0f, 0f, 0f, 0f)
adaptivePathResized.computeBounds(srcResizeRect, true)
resizeMatrix.reset()
resizeMatrix.setRectToRect(srcResizeRect, resizeRect, Matrix.ScaleToFit.CENTER)
adaptivePathResized.transform(resizeMatrix)
// We want to invert the path so we can clear it later
adaptivePathResized.fillType = Path.FillType.INVERSE_EVEN_ODD
}
}
Path enum/functions:
private val circlePath = Path().apply {
arcTo(RectF(0f, 0f, 50f, 50f), 0f, 359f)
close()
}
private val squirclePath = Path().apply { set(PathParser.createPathFromPathData("M 50,0 C 10,0 0,10 0,50 C 0,90 10,100 50,100 C 90,100 100,90 100,50 C 100,10 90,0 50,0 Z")) }
private val roundedPath = Path().apply { set(PathParser.createPathFromPathData("M 50,0 L 70,0 A 30,30,0,0 1 100,30 L 100,70 A 30,30,0,0 1 70,100 L 30,100 A 30,30,0,0 1 0,70 L 0,30 A 30,30,0,0 1 30,0 z")) }
private val squarePath = Path().apply {
lineTo(0f, 50f)
lineTo(50f, 50f)
lineTo(50f, 0f)
lineTo(0f, 0f)
close()
}
private val tearDropPath = Path().apply { set(PathParser.createPathFromPathData("M 50,0 A 50,50,0,0 1 100,50 L 100,85 A 15,15,0,0 1 85,100 L 50,100 A 50,50,0,0 1 50,0 z")) }
private val shieldPath = Path().apply { set(PathParser.createPathFromPathData("m6.6146,13.2292a6.6146,6.6146 0,0 0,6.6146 -6.6146v-5.3645c0,-0.6925 -0.5576,-1.25 -1.2501,-1.25L6.6146,-0 1.2501,-0C0.5576,0 0,0.5575 0,1.25v5.3645A6.6146,6.6146 0,0 0,6.6146 13.2292Z")) }
private val lemonPath = Path().apply { set(PathParser.createPathFromPathData("M1.2501,0C0.5576,0 0,0.5576 0,1.2501L0,6.6146A6.6146,6.6146 135,0 0,6.6146 13.2292L11.9791,13.2292C12.6716,13.2292 13.2292,12.6716 13.2292,11.9791L13.2292,6.6146A6.6146,6.6146 45,0 0,6.6146 0L1.2501,0z")) }
enum class IconPath(val path: () -> Path?) {
SYSTEM(
path = {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val adaptive = AdaptiveIconDrawable(null, null)
adaptive.iconMask
} else {
null
}
}
),
CIRCLE(path = { circlePath }),
SQUIRCLE(path = { squirclePath }),
ROUNDED(path = { roundedPath }),
SQUARE(path = { squarePath }),
TEARDROP(path = { tearDropPath }),
SHIELD(path = { shieldPath }),
LEMON(path = { lemonPath });
}
The key to copying the system preference is just to create an empty AdaptiveIconDrawable and read out the icon mask (which we later adjust the size of for use in the view. This will always return the current system icon shape path.
Sample usage:
myAdapativeImageView.setAdaptivePath(IconPath.SYSTEM.path())
Example:
Since Launcher is just an Activity, you can draw anything. You can draw application icons like ponies that run on beautiful animated clouds. This is your world, which obeys only your rules.
Further ... There is no magic in the programming world. If you are faced with magic, just use decompilers (with Java it very easy), find the code responsible for magic, document it and write a great blog post about how this magic works.
Given a AdaptiveIconDrawable instance, how do you shape it, to be of a
circular shape, rectangle, rounded rectangle, tear, and so on?
You can use AdaptiveIconDrawable.getBackground() and add any mask to it. Actually, you can do anything what you want with icon, AdaptiveIconDrawable is just way, where you can split foreground and background in easy way, without complicated filters or neural networks. Add parallax, animations and many more effects, now you have 2 layer for it.

Solving for calibration quaternion

I'm writing an Android app that requires the rotation vector. I'd like to use the TYPE_ROTATION_VECTOR but in some of my test devices the magnetometer doesn't perform well to say the least. Instead, the TYPE_GAME_ROTATION_VECTOR provides much smoother data (but I can't get direction relative to the Earth). What I ended up doing is while my data is loading, I run both virtual sensors. I now have an average quaternion for both, call them R (TYPE_ROTATION_VECTOR) and Rg (TYPE_GAME_ROTATION_VECTOR).
Once calibration is over I only run the TYPE_GAME_ROTATION_VECTOR, but would like to correct it for North. What I think I can do is something like: R = Rg * C where C is my calibration and Rg is the new TYPE_GAME_ROTATION_VECTOR data after a low pass filter. What I tried:
1. R = Rg * C
2. R * R' = Rg * C * R'
3. U = Rg * C * R' // Here U is the unit quaternion
4. C * R' = Rg' // This is because quaternion multiplication is associative
// Rg * (C * R') = U from line 3 therefore (C * R') must be
// equal to the conjugate of Rg
5. C = Rg' * R'' // I found this online somewhere (I hope this is right)
6. C = Rg' * R // R'' is just R
Now that I have C, I can take new values (after low pass filter) for the TYPE_GAME_ROTATION_VECTOR multiply them by C and get the actual rotation quaternion R that should be similar to the one that would have been provided by the TYPE_ROTATION_VECTOR with a steady North.
This gets me pretty close, but it doesn't quite work. I'm testing using a very simple AR like app that shows an item (who's position is determined by the device orientation) floating on the screen. If I leave out the calibration the character shows up and tracks perfectly, but it doesn't show up North of me (I have it fixed at (0, 1, 0) for now). If I take the rotation vector, get the quaternion, multiply by the calibration constant, the tracking gets thrown off:
Rotating the device about the Y axis shifts the item correctly horizontally, but it also adds a vertical component where rotating in the positive direction (using right hand rule) moves my item up (negative Y on the screen).
Rotating the device about the X axis shifts the item correctly vertically, but it also adds a horizontal component where rotation in the positive direction (using right hand rule) moves my item right (positive X on the screen).
Rotating the device about the Z axis works.
Sorry for the long description, I just want to make sure all the details are there. Summary of the question: I want to be able to get a rotation matrix that is roughly north and avoid using the magnetometer. I'm trying to do this by taking the average difference between TYPE_ROTATION_VECTOR and TYPE_GAME_ROTATION_VECTOR and using that to "calibrate" future values from the TYPE_GAME_ROTATION_VECTOR but it doesn't work. Does anyone know what the issue might be with how I'm calculating the calibration (or any other part of this)?
Some additional info:
private float[] values = null
public void onSensorChanged(SensorEvent event) {
values = lowPass(event.values.clone(), values);
Quaternion rawQuaternion = Quaternion.fromRotationVector(values);
Quaternion calibratedQuaternion = rawQuaternion.mult(calibration);
float[] rotationMatrix = calibratedQuaternion.getRotationMatrix();
float[] pos = new float[] { 0f, 1f, 0f, 1f };
Matrix.multiplyMV(pos, 0, rotationMatrix, 0, pos, 0);
Matrix.multiplyMV(pos, 0, matrixMVP, 0, pos, 0);
// Screen position should be found at pos[0], -pos[1] on a [-1,1] scale
}
Quaternion fromRotationVector(float[] r) {
float[] Q = new float[4];
SensorManager.getQuaternionFromVector(Q, r);
return new Quaternion(Q);
}
Quaternion mult(Quaternion q) {
Quaternion qu = new Quaternion();
qu.w = w*q.w - x*q.x - y*q.y - z*q.z;
qu.x = w*q.x + x*q.w + y*q.z - z*q.y;
qu.y = w*q.y + y*q.w + z*q.x - x*q.z;
qu.z = w*q.z + z*q.w + x*q.y - y*q.x;
return qu;
}
float[] getRotationMatrix() {
float[] M = new float[16];
float[] V = new float[] { x, y, z, w };
SensorManager.getRotationMatrixFromVector(M, V);
return M;
}
I had the same issue and did some research and realized where the problem is. So basically, by only looking at a stationary orientation of the IMU, you only align one axis of the coordinate system which is the vertical axis in the direction of gravity. That's why you rotations around Z axis works fine.
To complete your static calibrations, you have to include a planar motion and find the principal vectors of the motion which will be the, say, your X axis. Y axis follows the right-hand rule.
Simply, rotate the IMU around the global X axis and look at the gyroscope outputs of your IMU. The principal component of your gyroscope should be towards the X axis. After finding the Z axis in the first step and X axis in the second step, you can find Y axis by the cross product of the two. Using these axes, create the rotation matrix or the quaternion for the translations.
Here's what I ended up doing (there are some changes coming soon and once done I'll publish it on jcenter as a library). What this tries to solve is being able to run the Game Rotation Vector sensor (which has much less drift than the Rotation Vector sensor) while still pointing roughly north. Answer is in Kotlin:
class RotationMatrixLiveData(context Context): LiveData<FloatArray>(), SensorEventListener {
private val sensorManager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager
private val rotationSensor = sensorManager.getDefaultSensor(Sensor.TYPE_ROTATION_VECTOR)
private val gameRotationSensor =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2)
sensorManager.getDefaultSensor(Sensor.TYPE_GAME_ROTATION_VECTOR)
else null
private var isActive = false
private var isCalibrating = false
private var rotationValues: FloatArray? = null
var calibrationCount = 0
var calibrationQuaternion: FloatArray? = null
var calibrationGameCount = 0
var calibrationGameQuat: FloatArray? = null
var calibration: Quaternion? = null
var rotationQuaternionValues = FloatArray(4)
var gameQuaternionValues = FloatArray(4)
private val rotationVectorQuaternion = Quaternion()
init {
value = floatArrayOf(
1f, 0f, 0f, 0f,
0f, 1f, 0f, 0f,
0f, 0f, 1f, 0f,
0f, 0f, 0f, 1f)
}
/**
* Starts calibrating the rotation matrix (if the game rotation vector sensor
* is available.
*/
fun beginCalibration() {
gameRotationSensor?.let {
isCalibrating = true
calibration = null
calibrationQuaternion = null
calibrationCount = 0
calibrationGameQuat = null
calibrationGameCount = 0
sensorManager.registerListener(this, rotationSensor, SensorManager.SENSOR_DELAY_FASTEST)
sensorManager.registerListener(this, it, SensorManager.SENSOR_DELAY_FASTEST)
}
}
/**
* Stop calibrating the rotation matrix.
*/
fun stopCalibration() {
isCalibrating = false
if (!isActive) {
// Not active, just turn off everthing
sensorManager.unregisterListener(this)
} else if (gameRotationSensor != null) {
// Active and has both sensors, turn off rotation and leave the game rotation running
sensorManager.unregisterListener(this, rotationSensor)
}
}
override fun onActive() {
super.onActive()
isActive = true
val sensor = gameRotationSensor ?: rotationSensor
sensorManager.registerListener(this, sensor, SensorManager.SENSOR_DELAY_FASTEST)
}
override fun onInactive() {
super.onInactive()
isActive = false
if (!isCalibrating) {
sensorManager.unregisterListener(this)
}
}
//
// SensorEventListener
//
override fun onAccuracyChanged(sensor: Sensor, accuracy: Int) {}
override fun onSensorChanged(event: SensorEvent) {
if (isCalibrating) {
if (event.sensor.type == Sensor.TYPE_ROTATION_VECTOR) {
SensorManager.getQuaternionFromVector(rotationQuaternionValues, event.values)
calibrationQuaternion?.let { quat ->
for (i in 0..3) {
rotationQuaternionValues[i] += quat[i]
}
}
calibrationQuaternion = rotationQuaternionValues
calibrationCount++
} else if (event.sensor.type == Sensor.TYPE_GAME_ROTATION_VECTOR) {
SensorManager.getQuaternionFromVector(gameQuaternionValues, event.values)
calibrationGameQuat?.let {quat ->
for (i in 0..3) {
gameQuaternionValues[i] += quat[i]
}
}
calibrationGameQuat = gameQuaternionValues
calibrationGameCount++
}
} else if (gameRotationSensor == null || event.sensor.type != Sensor.TYPE_ROTATION_VECTOR) {
// Only calculate rotation if there is no game rotation sensor or if the event is a game
// rotation
val calibrationQ = calibrationQuaternion
val calibrationQg = calibrationGameQuat
if (calibrationQ != null && calibrationQg != null) {
for (i in 0..3) {
calibrationQ[i] /= calibrationCount.toFloat()
calibrationQg[i] /= calibrationGameCount.toFloat()
}
calibration = (Quaternion(calibrationQg).apply { conjugate() } *
Quaternion(calibrationQ)).apply {
x = 0f
y = 0f
normalize()
}
}
calibrationQuaternion = null
calibrationGameQuat = null
// Run values through low-pass filter
val values = lowPass(event.values, rotationValues)
rotationValues = values
rotationVectorQuaternion.setFromRotationVector(values)
// Calibrate if available
calibration?.let { rotationVectorQuaternion.preMult(it) }
// Generate rotation matrix
value = rotationVectorQuaternion.getRotationMatrix(value)
}
}
}
For the quaternion class I'm using:
class Quaternion(val values: FloatArray = floatArrayOf(1f, 0f, 0f, 0f)) {
companion object {
fun fromRotationVector(rv: FloatArray): Quaternion {
val Q = FloatArray(4)
SensorManager.getQuaternionFromVector(Q, rv)
return Quaternion(Q)
}
}
private val buffer = FloatArray(4)
var w: Float
get() = values[0]
set(value) { values[0] = value }
var x: Float
get() = values[1]
set(value) { values[1] = value }
var y: Float
get() = values[2]
set(value) { values[2] = value }
var z: Float
get() = values[3]
set(value) { values[3] = value }
fun setFromRotationVector(rv: FloatArray) {
SensorManager.getQuaternionFromVector(values, rv)
}
fun conjugate() {
x = -x
y = -y
z = -z
}
fun getRotationMatrix(R: FloatArray? = null): FloatArray {
val matrix = R ?: FloatArray(16)
for (i in 0..3) {
buffer[i] = values[(i+1)%4]
}
SensorManager.getRotationMatrixFromVector(matrix, buffer)
return matrix
}
fun magnitude(): Float {
var mag = 0f
for (i in 0..3) {
mag += values[i]*values[i]
}
return Math.sqrt(mag.toDouble()).toFloat()
}
fun normalize() {
val mag = magnitude()
x /= mag
y /= mag
z /= mag
w /= mag
}
fun preMult(left: Quaternion) {
buffer[0] = left.w*this.w - left.x*this.x - left.y*this.y - left.z*this.z
buffer[1] = left.w*this.x + left.x*this.w + left.y*this.z - left.z*this.y
buffer[2] = left.w*this.y + left.y*this.w + left.z*this.x - left.x*this.z
buffer[3] = left.w*this.z + left.z*this.w + left.x*this.y - left.y*this.x
for (i in 0..3) {
values[i] = buffer[i]
}
}
operator fun times(q: Quaternion): Quaternion {
val qu = Quaternion()
qu.w = w*q.w - x*q.x - y*q.y - z*q.z
qu.x = w*q.x + x*q.w + y*q.z - z*q.y
qu.y = w*q.y + y*q.w + z*q.x - x*q.z
qu.z = w*q.z + z*q.w + x*q.y - y*q.x
return qu
}
operator fun times(v: FloatArray): FloatArray {
val conj = Quaternion(values.clone()).apply { conjugate() }
return multiplyQV(multiplyQV(values, v), conj.values)
}
override fun toString(): String {
return "(${w.toString(5)}(w), ${x.toString(5)}, ${y.toString(5)}, ${z.toString(5)}) |${magnitude().toString(5)}|"
}
private fun multiplyQV(q: FloatArray, r: FloatArray): FloatArray {
val result = FloatArray(4)
result[0] = r[0]*q[0]-r[1]*q[1]-r[2]*q[2]-r[3]*q[3]
result[1] = r[0]*q[1]+r[1]*q[0]-r[2]*q[3]+r[3]*q[2]
result[2] = r[0]*q[2]+r[1]*q[3]+r[2]*q[0]-r[3]*q[1]
result[3] = r[0]*q[3]-r[1]*q[2]+r[2]*q[1]+r[3]*q[0]
return result
}
}

Categories

Resources