I am trying to create a circle drawable with a dash. I am able to achieve circle with dash but am not able to apply gradient color. Is there any way to do it? thanks in advance.
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<size
android:width="#dimen/size60"
android:height="#dimen/size60" />
<stroke
android:width="1dp"
android:color="#color/white"
android:dashWidth="3dp"
android:dashGap="1dp" />
</shape>
View achieved:
View Required:
There is not a way to create a gradient ring using just XML AFAIK. You'll have better luck using a custom drawable. The following combine a sweep gradient shader with a Paint object to create a ring that has a gradient from start to end.
class DashedRingDrawable : Drawable() {
private val mPaint = Paint().apply {
style = Paint.Style.STROKE
strokeWidth = STROKE_WIDTH
}
private val mColorArray = intArrayOf(Color.WHITE, Color.BLACK)
private var mRingOuterDiameter = 0f
private var mRingOuterRadius = 0f
private var mRingInnerRadius = 0f
override fun onBoundsChange(bounds: Rect) {
super.onBoundsChange(bounds)
check(bounds.width() == bounds.height()) {
"Width must be equal to height. (It's a circle.)"
}
mRingOuterDiameter = bounds.width().toFloat()
mRingOuterRadius = mRingOuterDiameter / 2
mRingInnerRadius = (mRingOuterDiameter - STROKE_WIDTH) / 2
val dashLength = getNewDashLength()
mPaint.pathEffect = DashPathEffect(floatArrayOf(dashLength, GAP_LENGTH), 0f)
mPaint.shader = SweepGradient(mRingOuterRadius, mRingOuterRadius, mColorArray, null)
}
override fun draw(canvas: Canvas) {
// The following statement is here to show the boundaries and can be removed/commented out.
canvas.drawColor(Color.RED)
canvas.drawCircle(mRingOuterRadius, mRingOuterRadius, mRingInnerRadius, mPaint)
}
override fun setAlpha(alpha: Int) {
}
override fun setColorFilter(colorFilter: ColorFilter?) {
}
override fun getOpacity(): Int {
return PixelFormat.OPAQUE
}
// Adjust the dash length so that we end on a gap and not in the middle of a dash.
private fun getNewDashLength(): Float {
val circumference = Math.PI.toFloat() * mRingInnerRadius * 2
val dashCount = (circumference / (DASH_LENGTH + GAP_LENGTH)).toInt()
val newDashLength = (circumference - dashCount * GAP_LENGTH) / dashCount
return newDashLength
}
companion object {
const val STROKE_WIDTH = 15f
const val DASH_LENGTH = 50f
const val GAP_LENGTH = 15f
}
}
For API 24 and higher, you can place this drawable into an XML file and use it like any other XML drawable.
<drawable xmlns:android="http://schemas.android.com/apk/res/android"
class="com.example.myapp.DashedRingDrawable"/>
For APIs before API 24, you will need to work with this custom drawable programmatically.
Related
I would like to make such background for my textView:
I managed to make only with outer semi circle:
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#color/p_color" />
<corners android:radius="24dp" />
</shape>
which is not good for me. I also tried to work with states list but didn't succeed with it.
Hello my Friend,
What you need will not create by shape Drawable, you need a custome view
in fact you need some trick to make your desired shape by custom views and use it inside a constraintlayout as background of your TextView
I coded for you by kotlin(if you need java inform me):
first:
make a file and name it CustomeBg copy below code put in that:
import android.content.Context
import android.graphics.*
import android.util.AttributeSet
import android.view.View
class CustomeBg #JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
private var W = 0.0f
private var H = 0.0f
/////main body Rect and Paint
private var bodyRect = Rect()
private var bodyRectPaint = Paint(Paint.ANTI_ALIAS_FLAG)
//right half circle Rect and Paint
private var rightCircleRect = RectF()
private var rightCirclePaint = Paint(Paint.ANTI_ALIAS_FLAG)
//left half circle Rect and Paint
private var leftCircleRect = RectF()
private var leftCirclePaint = Paint(Paint.ANTI_ALIAS_FLAG)
init {
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
W = w.toFloat()
H = h.toFloat()
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
// drawRect(canvas)
var left = 0
var top = 0
var right = W - RADIUS_RIGHT
var bottom = H
bodyRectPaint.apply {
color = BODY_COLOR_RECT
style = Paint.Style.FILL
}
bodyRect.set(left, top, right.toInt(), bottom.toInt())
canvas.drawRect(bodyRect, bodyRectPaint)
// Draw Left half-Circle
var left_l_circle = -RADIUS_LEFT
var top_l_circle = 0f
var right_l_circle = RADIUS_LEFT
var bottom_l_circle = H
leftCirclePaint.apply {
color = LEFT_CIRCLE_COLOR
style = Paint.Style.FILL
}
leftCircleRect = RectF(left_l_circle, top_l_circle, right_l_circle, bottom_l_circle)
canvas.drawArc(leftCircleRect, -90f, 180f, true, leftCirclePaint)
// Draw Right half-Circle
var left_r_circle = (bodyRect.right) - RADIUS_RIGHT
var top_r_circle = 0f
var right_r_circle = bodyRect.right + RADIUS_RIGHT
var bottom_r_circle = H
rightCirclePaint.apply {
color = RIGHT_CIRCLE_COLOR
style = Paint.Style.FILL
}
rightCircleRect = RectF(left_r_circle, top_r_circle, right_r_circle, bottom_r_circle)
canvas.drawArc(rightCircleRect, -90f, 180f, true, rightCirclePaint)
}
companion object {
private val BODY_COLOR_RECT = Color.parseColor("#00534b")
private val LEFT_CIRCLE_COLOR = Color.parseColor("#ffffff")
private val RIGHT_CIRCLE_COLOR = Color.parseColor("#00534b")
private val RADIUS_LEFT = 100f
private val RADIUS_RIGHT = 100f
}
}
second:
then in your main_activity layout put below XML codes:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:layout_editor_absoluteX="51dp"
tools:layout_editor_absoluteY="95dp">
<com.example.junk2.CustomeBg
android:id="#+id/itemSettingBg"
android:layout_width="match_parent"
android:layout_height="50dp"
android:layout_marginStart="30dp"
android:layout_marginEnd="30dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
</com.example.junk2.CustomeBg>
<TextView
android:id="#+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hallo ich bin Custome"
android:textColor="#FFFFFF"
app:layout_constraintBottom_toBottomOf="#+id/itemSettingBg"
app:layout_constraintEnd_toEndOf="#+id/itemSettingBg"
app:layout_constraintStart_toStartOf="#+id/itemSettingBg"
app:layout_constraintTop_toTopOf="#+id/itemSettingBg" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
Result is :
I need explain a little my codes for better understanding:
in CustomeBg there are some value:
companion object {
private val BODY_COLOR_RECT = Color.parseColor("#00534b")
private val LEFT_CIRCLE_COLOR = Color.parseColor("#ffffff")
private val RIGHT_CIRCLE_COLOR = Color.parseColor("#00534b")
private val RADIUS_LEFT = 100f
private val RADIUS_RIGHT = 100f
}
in this file in fact we have 3 object, two half-circle(one for left and one for right)
and we have one rectangular as our body, above code explain radius for every circle and their colors, you can see body color here too
Notice: you can change above value for your usage my freiend
I'm making a small project, but yet different. So I found out that the bitmap works when the image is implemented from Android Studio and include it as resources(example: BitmapFactory.decodeResources(resources, R.drawable.image.jpg)) in order for the watermark to work, however I am struggling with something similar, but selecting a certain image from the android device, using by code of course, but later one probably gonna be questions once the gallery is selected to automatically place the watermark, but like I said that's a different question for another time. My question is there any possible way, when I press the watermark button, to automatically locate and place the image from the Android device on to the imageview, after that to place the text watermark ?
Also on the console log, I'm receiving this message: E/BitmapFactory: Unable to decode stream: java.io.FileNotFoundException: /data/user/0/com.example.cameraxapp/files (Is a directory)
If any of you know the issue that I am having, please let me know and examples, will also be helpful. Thank you in advance!
Manifest
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" tools:ignore="ScopedStorage" />
ActivityGallery.xml(It can be even in MainActivity.xml)
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#8A8A8A"
android:backgroundTint="#8A8A8A"
tools:context=".ActivityGallery">
<Button
android:id="#+id/watermarkSetBtn"
android:layout_width="75dp"
android:layout_height="wrap_content"
android:backgroundTint="#4CAF50"
android:text="#string/watermark"
android:textSize="11sp"
app:layout_constraintTop_toBottomOf="#id/imageViewPhoto"
app:layout_constraintEnd_toStartOf="#id/saveButton"
app:layout_constraintStart_toEndOf="#id/pickPhotoButton"
app:layout_constraintBottom_toBottomOf="parent"
/>
<ImageView
android:id="#+id/imageViewPhoto"
android:layout_width="match_parent"
android:layout_height="670dp"
android:importantForAccessibility="no"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="#android:drawable/ic_menu_gallery" />
</androidx.constraintlayout.widget.ConstraintLayout>
GalleryActivity.kt
The main function for the watermark as Bitmap
private fun addWatermark(bitmap: Bitmap, watermarkText: String, options: WatermarkOptions = WatermarkOptions()): Bitmap {
val result = bitmap.copy(bitmap.config, true)
val canvas = Canvas(result)
val paint = Paint(ANTI_ALIAS_FLAG or DITHER_FLAG)
paint.textAlign = when (options.corner) {
Corner.TOP_LEFT,
Corner.BOTTOM_LEFT -> Paint.Align.LEFT
Corner.TOP_RIGHT,
Corner.BOTTOM_RIGHT -> Paint.Align.RIGHT
}
val textSize = result.width * options.textSizeToWidthRatio
paint.textSize = textSize
paint.color = options.textColor
if (options.shadowColor != null) {
paint.setShadowLayer(textSize / 2, 0f, 0f, options.shadowColor)
}
if (options.typeface != null) {
paint.typeface = options.typeface
}
val padding = result.width * options.paddingToWidthRatio
val coordinates = calculateCoordinates(watermarkText, paint, options, canvas.width, canvas.height, padding)
canvas.drawText(watermarkText, coordinates.x, coordinates.y, paint)
return result
}
private fun calculateCoordinates(watermarkText: String, paint: Paint, options: WatermarkOptions, width: Int, height: Int, padding: Float): PointF {
val x = when (options.corner) {
Corner.TOP_LEFT,
Corner.BOTTOM_LEFT -> {
padding
}
Corner.TOP_RIGHT,
Corner.BOTTOM_RIGHT -> {
width - padding
}
}
val y = when (options.corner) {
Corner.BOTTOM_LEFT,
Corner.BOTTOM_RIGHT -> {
height - padding
}
Corner.TOP_LEFT,
Corner.TOP_RIGHT -> {
val bounds = Rect()
paint.getTextBounds(watermarkText, 0, watermarkText.length, bounds)
val textHeight = bounds.height()
textHeight + padding
}
}
return PointF(x, y)
}
enum class Corner {
TOP_LEFT,
TOP_RIGHT,
BOTTOM_LEFT,
BOTTOM_RIGHT
}
data class WatermarkOptions(
val corner: Corner = Corner.BOTTOM_RIGHT,
val textSizeToWidthRatio: Float = 0.04f,
val paddingToWidthRatio: Float = 0.03f,
#ColorInt val textColor: Int = Color.WHITE,
#ColorInt val shadowColor: Int? = Color.BLACK,
val typeface: Typeface? = null
)
companion object {
//private const val FILENAME_FORMAT = "yyyy.MM.dd-HH"
private const val TAG = "CameraXBasic"
}
Placing it on onCreate method
private lateinit var mImageView: ImageView
private lateinit var mWatermarkSet: Button
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_gallery)
//Views
mImageView = findViewById(R.id.imageViewPhoto)
mWatermarkSet = findViewById(R.id.watermarkSetBtn)
mWatermarkSet.setOnClickListener {
if (filesDir.isDirectory) {
val internalStorage = filesDir.absolutePath
val pathName = "$internalStorage"
//resources, R.drawable.gefvqkbbslamdpkxf1zv_bigstock_aerial_view_of_blue_lakes_and__227291596
val originalBitmap = BitmapFactory.decodeFile(pathName)?.let { it ->
addWatermark(
it,
"#Test",
WatermarkOptions(
Corner.TOP_RIGHT,
textSizeToWidthRatio = 0.03f,
paddingToWidthRatio = 0.03f,
Color.GREEN,
Color.BLACK,
typeface = null
)
)
}
mImageView.setImageBitmap(originalBitmap)
}
}
}
I know there are already a bunch of questions related to the "trying to use a recycled bitmap" crash, but none helped me.
Details:
There are no calls to Bitmap.recycle() anywhere in this project
All images are loaded using Glide (4.11.0)
Glide calls are all simple, and not using placeholders
The crash seems to happen randomly when switching fragments
Only thing I could think of were the transformations.
There are only 2 transformations in this project.
CircleTransformation (clip image as a circle with a custom radius):
class CircleTransformation(private val radius: Float) : BitmapTransformation() {
companion object {
private const val ID = "com.project.transformation.circle"
private val ID_BYTES: ByteArray = ID.toByteArray()
}
public override fun transform(pool: BitmapPool, source: Bitmap, outWidth: Int, outHeight: Int): Bitmap {
val paint = Paint()
paint.isAntiAlias = true
paint.shader = BitmapShader(source, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP)
val halfWidth = source.width / 2f
val output = Bitmap.createBitmap(source.width, source.height, Bitmap.Config.ARGB_8888)
val canvas = Canvas(output)
canvas.drawCircle(
halfWidth,
(source.height / 2).toFloat(),
halfWidth * radius,
paint
)
return output
}
// Caching helpers
override fun equals(other: Any?): Boolean {
return other is CircleTransformation && other.hashCode() == hashCode()
}
override fun hashCode(): Int {
return ID.hashCode()
}
override fun updateDiskCacheKey(messageDigest: MessageDigest) {
messageDigest.update(ID_BYTES)
}
}
And ClipWhiteTransformation (remove white border from image):
class ClipWhiteTransformation() : BitmapTransformation() {
companion object {
private const val ID = "com.project.transformation.clipWhite"
private val ID_BYTES: ByteArray = ID.toByteArray()
// Config
const val white = 253 // White pixel, if all channels are equal or greater than this
const val transparent = 50 // Transparent pixel, if Less than this
}
public override fun transform(pool: BitmapPool, source: Bitmap, outWidth: Int, outHeight: Int): Bitmap {
val width = source.width - 1
val height = source.height - 1
val halfX = width / 2
val halfY = height / 2
var startY = 0
// Left Margin
var left = 0
for (x in 0 until halfX) {
val pixel = source.getPixel(x, halfY)
// Transparent?
if (Color.alpha(pixel) < transparent) continue
// Not white?
if (Color.red(pixel) < white || Color.green(pixel) < white || Color.blue(pixel) < white) {
left = x
if (x > 2) {
startY = 2
}
break
}
}
// Right Margin
var right = 0
for (x in 0 until halfX) {
val pixel = source.getPixel(width - x, halfY)
// Transparent?
if (Color.alpha(pixel) < transparent) continue
// Not white?
if (Color.red(pixel) < white || Color.green(pixel) < white || Color.blue(pixel) < white) {
right = x
if (x > 2) {
startY = 2
}
break
}
}
// Top Margin
var top = 0
for (y in startY until halfY) {
val pixel = source.getPixel(halfX, y)
// Transparent?
if (Color.alpha(pixel) < transparent) continue
// Not white?
if (Color.red(pixel) < white || Color.green(pixel) < white || Color.blue(pixel) < white) {
top = y
break
}
}
// Bottom Margin
var bottom = 0
for (y in startY until halfY) {
val pixel = source.getPixel(halfX, height - y)
// Transparent?
if (Color.alpha(pixel) < transparent) continue
// Not white?
if (Color.red(pixel) < white || Color.green(pixel) < white || Color.blue(pixel) < white) {
bottom = y
break
}
}
// Clip, scale and return
val newWidth = width - (left + right)
val newHeight = height - (top + bottom)
val scale = if (abs(newWidth - outWidth) > abs(newHeight - outHeight)) outWidth / newWidth.toFloat() else outHeight / newHeight.toFloat()
val matrix = Matrix().apply { setScale(scale, scale) }
return Bitmap.createBitmap(source, left, top, newWidth, newHeight, matrix, false)
}
// Caching helpers
override fun equals(other: Any?): Boolean {
return other is ClipWhiteTransformation && other.hashCode() == hashCode()
}
override fun hashCode(): Int {
return ID.hashCode()
}
override fun updateDiskCacheKey(messageDigest: MessageDigest) {
messageDigest.update(ID_BYTES)
}
}
Was using the BitmapPool initially, removing it didn't stop the crash.
By the way, this is the extension used to load images:
fun ImageView.setURL(url: String,
#DrawableRes error: Int? = null,
#DrawableRes placeholder: Int? = null,
size: Int? = null,
options: ((RequestBuilder<Drawable>) -> Unit)? = null,
completion: ((resource: Drawable?) -> Unit)? = null) {
// No URL, use Placeholder if exists, if not, use the error image
if (url.isEmpty()) {
placeholder?.also{ setImageResource(it) } ?: run { error?.also{ setImageResource(it) } }
return
}
Glide.with(applicationInstance) // (I'm using an application instance directly here)
.load(url).apply {
completion?.also { completion ->
this.listener(object : RequestListener<Drawable> {
override fun onLoadFailed(e: GlideException?, model: Any?, target: Target<Drawable>?, isFirstResource: Boolean): Boolean {
completion(null)
return false
}
override fun onResourceReady(resource: Drawable?, model: Any?, target: Target<Drawable>?, dataSource: DataSource?, isFirstResource: Boolean): Boolean {
completion(resource)
return false
}
})
}
}
.apply { size?.also { this.override(it)} }
.apply { options?.invoke(this) }
.placeholder(placeholder ?: 0)
.error(error ?: 0)
.transition(DrawableTransitionOptions.withCrossFade(350))
.into(this)
}
Sorry for pasting so much code here (hope it's useful to someone).
Can these transformations or loader cause the crash?
To shape(circle/square/oval) your image You do not need to Transform your image .
MaterialDesign has introduce ShapeableImageView which let you shape your image at runtime, also you can add border with color .
add matrial dependecies :
implementation 'com.google.android.material:material:1.3.0-alpha01'
Add shapeableImageView in your xyz.xml:
<com.google.android.material.imageview.ShapeableImageView
android:id="#+id/imgStudent"
android:layout_width="100dp"
android:layout_height="100dp"
app:shapeAppearanceOverlay="#style/circleImageView"
android:padding="2dp"
app:strokeColor="#color/white"
app:strokeWidth="5dp"
android:scaleType="centerCrop"
android:adjustViewBounds="true"
tools:srcCompat="#drawable/ic_kid_placeholder"
/>
Add style inside res/values/style.xml file
<style name="circleImageView" parent="">
<item name="cornerFamily">rounded</item>
<item name="cornerSize">50%</item>
<item name="android:shadowRadius">100</item>
<item name="android:shadowColor">#color/gray</item>
<item name="backgroundOverlayColorAlpha">12</item>
</style>
And at last load your image with glide .
I want to create a battery level indicator as in the image(which i circled). The green part should fill based on the available battery in the device.
Getting the battery percentage from the device like this
registerReceiver(mBatInfoReceiver, new IntentFilter(
Intent.ACTION_BATTERY_CHANGED));
So in the layout i am able to display the battery percentage.
public class BatteryIndicatorActivity extends Activity {
//Create Broadcast Receiver Object along with class definition
private BroadcastReceiver mBatInfoReceiver = new BroadcastReceiver() {
#Override
//When Event is published, onReceive method is called
public void onReceive(Context c, Intent i) {
//Get Battery %
int level = i.getIntExtra("level", 0);
TextView tv = (TextView) findViewById(R.id.textfield);
//Set TextView with text
tv.setText("Battery Level: " + Integer.toString(level) + "%");
}
};
But how to create a UI for this type of battery indicator. Is their any api to achieve this?, If not how to create such type of UI.
Your help will be appreciated.
Here is my CustomView for display battery level
class BatteryView #JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) :
View(context, attrs, defStyleAttr) {
private var radius: Float = 0f
private var isCharging: Boolean = false
// Top
private var topPaint =
PaintDrawable(Color.WHITE) // I only want to corner top-left and top-right so I use PaintDrawable instead of Paint
private var topRect = Rect()
private var topPaintWidthPercent = 50
private var topPaintHeightPercent = 8
// Border
private var borderPaint = Paint().apply {
color = Color.BLUE
style = Paint.Style.STROKE
}
private var borderRect = RectF()
private var borderStrokeWidthPercent = 8
private var borderStroke: Float = 0f
// Percent
private var percentPaint = Paint()
private var percentRect = RectF()
private var percentRectTopMin = 0f
private var percent: Int = 0
// Charging
private var chargingRect = RectF()
private var chargingBitmap: Bitmap? = null
init {
init(attrs)
chargingBitmap = getBitmap(R.drawable.ic_charging)
}
private fun init(attrs: AttributeSet?) {
val ta = context.obtainStyledAttributes(attrs, R.styleable.BatteryView)
try {
percent = ta.getInt(R.styleable.BatteryView_bv_percent, 0)
isCharging = ta.getBoolean(R.styleable.BatteryView_bv_charging, false)
} finally {
ta.recycle()
}
}
#SuppressLint("DrawAllocation")
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val measureWidth = View.getDefaultSize(suggestedMinimumWidth, widthMeasureSpec)
val measureHeight = (measureWidth * 1.8f).toInt()
setMeasuredDimension(measureWidth, measureHeight)
radius = borderStroke / 2
borderStroke = (borderStrokeWidthPercent * measureWidth).toFloat() / 100
// Top
val topLeft = measureWidth * ((100 - topPaintWidthPercent) / 2) / 100
val topRight = measureWidth - topLeft
val topBottom = topPaintHeightPercent * measureHeight / 100
topRect = Rect(topLeft, 0, topRight, topBottom)
// Border
val borderLeft = borderStroke / 2
val borderTop = topBottom.toFloat() + borderStroke / 2
val borderRight = measureWidth - borderStroke / 2
val borderBottom = measureHeight - borderStroke / 2
borderRect = RectF(borderLeft, borderTop, borderRight, borderBottom)
// Progress
val progressLeft = borderStroke
percentRectTopMin = topBottom + borderStroke
val progressRight = measureWidth - borderStroke
val progressBottom = measureHeight - borderStroke
percentRect = RectF(progressLeft, percentRectTopMin, progressRight, progressBottom)
// Charging Image
val chargingLeft = borderStroke
var chargingTop = topBottom + borderStroke
val chargingRight = measureWidth - borderStroke
var chargingBottom = measureHeight - borderStroke
val diff = ((chargingBottom - chargingTop) - (chargingRight - chargingLeft))
chargingTop += diff / 2
chargingBottom -= diff / 2
chargingRect = RectF(chargingLeft, chargingTop, chargingRight, chargingBottom)
}
override fun onDraw(canvas: Canvas) {
drawTop(canvas)
drawBody(canvas)
if (!isCharging) {
drawProgress(canvas, percent)
} else {
drawCharging(canvas)
}
}
private fun drawTop(canvas: Canvas) {
topPaint.bounds = topRect
topPaint.setCornerRadii(floatArrayOf(radius, radius, radius, radius, 0f, 0f, 0f, 0f))
topPaint.draw(canvas)
}
private fun drawBody(canvas: Canvas) {
borderPaint.strokeWidth = borderStroke
canvas.drawRoundRect(borderRect, radius, radius, borderPaint)
}
private fun drawProgress(canvas: Canvas, percent: Int) {
percentPaint.color = getPercentColor(percent)
percentRect.top = percentRectTopMin + (percentRect.bottom - percentRectTopMin) * (100 - percent) / 100
canvas.drawRect(percentRect, percentPaint)
}
// todo change color
private fun getPercentColor(percent: Int): Int {
if (percent > 50) {
return Color.WHITE
}
if (percent > 30) {
return Color.YELLOW
}
return Color.RED
}
private fun drawCharging(canvas: Canvas) {
chargingBitmap?.let {
canvas.drawBitmap(it, null, chargingRect, null)
}
}
private fun getBitmap(drawableId: Int, desireWidth: Int? = null, desireHeight: Int? = null): Bitmap? {
val drawable = AppCompatResources.getDrawable(context, drawableId) ?: return null
val bitmap = Bitmap.createBitmap(
desireWidth ?: drawable.intrinsicWidth,
desireHeight ?: drawable.intrinsicHeight,
Bitmap.Config.ARGB_8888
)
val canvas = Canvas(bitmap)
drawable.setBounds(0, 0, canvas.width, canvas.height)
drawable.draw(canvas)
return bitmap
}
fun charge() {
isCharging = true
invalidate() // can improve by invalidate(Rect)
}
fun unCharge() {
isCharging = false
invalidate()
}
fun setPercent(percent: Int) {
if (percent > 100 || percent < 0) {
return
}
this.percent = percent
invalidate()
}
fun getPercent(): Int {
return percent
}
}
style.xml
<declare-styleable name="BatteryView">
<attr name="bv_charging" format="boolean" />
<attr name="bv_percent" format="integer" />
</declare-styleable>
drawable/ic_charging.xml
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="368.492"
android:viewportHeight="368.492">
<path
android:fillColor="#FFFFFF"
android:pathData="M297.51,150.349c-1.411,-2.146 -3.987,-3.197 -6.497,-2.633l-73.288,16.498L240.039,7.012c0.39,-2.792 -1.159,-5.498 -3.766,-6.554c-2.611,-1.069 -5.62,-0.216 -7.283,2.054L71.166,217.723c-1.489,2.035 -1.588,4.773 -0.246,6.911c1.339,2.132 3.825,3.237 6.332,2.774l79.594,-14.813l-23.257,148.799c-0.436,2.798 1.096,5.536 3.714,6.629c0.769,0.312 1.562,0.469 2.357,0.469c1.918,0 3.78,-0.901 4.966,-2.517l152.692,-208.621C298.843,155.279 298.916,152.496 297.51,150.349z" />
</vector>
Using
<package.BatteryView
android:id="#+id/battery_view"
android:layout_width="150dp"
android:layout_height="wrap_content"
android:src="#drawable/ic_charging"
app:bv_charging="false"
app:bv_percent="20" />
Github project
Hope it help
There are a lot of ways to do it. Here are few of them:
Use a ProgressBar, make it vertical, make it be drawn in the battery shape
Use a custom View, override onDraw() in it, and draw the battery shape on the canvas
Use a white image with a transparent battery shape. Place it over a view, where you fill a background vertically, or change background view's height.
I'm trying to replace menu icon with cross icon, but I don't know better solution than replacing source in ImageView and futher more I cannot find done libraries which converts images.
Any help will be appreciateg.
Android has a drawable for animating between hamburger and arrow: android.support.v7.graphics.drawable.DrawerArrowDrawable
This drawable uses very generic approach with canvas drawing. If you have some spare time and ready for some tedious work, you can animate pretty much anything by looking at this example.
For instance, here is "hamburger" to cross drawable:
/**
* Simple animating drawable between the "hamburger" icon and cross icon
*
* Based on [android.support.v7.graphics.drawable.DrawerArrowDrawable]
*/
class HamburgerCrossDrawable(
/** Width and height of the drawable (the drawable is always square) */
private val size: Int,
/** Thickness of each individual line */
private val barThickness: Float,
/** The space between bars when they are parallel */
private val barGap: Float
) : Drawable() {
private val paint = Paint()
private val thick2 = barThickness / 2.0f
init {
paint.style = Paint.Style.STROKE
paint.strokeJoin = Paint.Join.MITER
paint.strokeCap = Paint.Cap.BUTT
paint.isAntiAlias = true
paint.strokeWidth = barThickness
}
override fun draw(canvas: Canvas) {
if (progress < 0.5) {
drawHamburger(canvas)
} else {
drawCross(canvas)
}
}
private fun drawHamburger(canvas: Canvas) {
val bounds = bounds
val centerY = bounds.exactCenterY()
val left = bounds.left.toFloat() + thick2
val right = bounds.right.toFloat() - thick2
// Draw middle line
canvas.drawLine(
left, centerY,
right, centerY,
paint)
// Calculate Y offset to top and bottom lines
val offsetY = barGap * (2 * (0.5f - progress))
// Draw top & bottom lines
canvas.drawLine(
left, centerY - offsetY,
right, centerY - offsetY,
paint)
canvas.drawLine(
left, centerY + offsetY,
right, centerY + offsetY,
paint)
}
private fun drawCross(canvas: Canvas) {
val bounds = bounds
val centerX = bounds.exactCenterX()
val centerY = bounds.exactCenterY()
val crossHeight = barGap * 2 + barThickness * 3
val crossHeight2 = crossHeight / 2
// Calculate current cross position
val distanceY = crossHeight2 * (2 * (progress - 0.5f))
val top = centerY - distanceY
val bottom = centerY + distanceY
val left = centerX - crossHeight2
val right = centerX + crossHeight2
// Draw cross
canvas.drawLine(
left, top,
right, bottom,
paint)
canvas.drawLine(
left, bottom,
right, top,
paint)
}
override fun setAlpha(alpha: Int) {
if (alpha != paint.alpha) {
paint.alpha = alpha
invalidateSelf()
}
}
override fun setColorFilter(colorFilter: ColorFilter?) {
paint.colorFilter = colorFilter
invalidateSelf()
}
override fun getIntrinsicWidth(): Int {
return size
}
override fun getIntrinsicHeight(): Int {
return size
}
override fun getOpacity(): Int {
return PixelFormat.TRANSLUCENT
}
/**
* Drawable color
* Can be animated
*/
var color: Int = 0xFFFFFFFF.toInt()
set(value) {
field = value
paint.color = value
invalidateSelf()
}
/**
* Animate this property to transition from hamburger to cross
* 0 = hamburger
* 1 = cross
*/
var progress: Float = 0.0f
set(value) {
field = value.coerceIn(0.0f, 1.0f)
invalidateSelf()
}
}
You can use this drawable like any other drawable, for example by setting ImageView src to this drawable:
imageView = AppCompatImageView(context)
addView(imageView, LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
hamburgerDrawable = HamburgerCrossDrawable(
size = dpToPx(20).toInt(),
barThickness = dpToPx(2),
barGap = dpToPx(5)
)
hamburgerDrawable.color = hamburgerColor
imageView.setImageDrawable(hamburgerDrawable)
To make the drawable actually change from hamburger to cross and back you'll need to change HamburgerCrossDrawable.progress (0 stands for hamburger and 1 stands for cross):
val animator = ValueAnimator.ofFloat(0.0f, 1.0f)
animator.interpolator = AccelerateDecelerateInterpolator()
animator.duration = 300
animator.addUpdateListener {
val progress = it.animatedValue as Float
val color = interpolateColor(hamburgerColor, crossColor, progress)
hamburgerDrawable.color = color
hamburgerDrawable.progress = progress
}
animator.start()
I seem to be a little late to the show, but I prefer a declarative approach with an animated selector for icon animations. It seems a lot clearer and the only thing you need to care about are the View or Button's states.
TL;DR: I've created a gist with all of the classes needed to achieve the animation.
Here's an example of the selector which you use as a drawable:
<?xml version="1.0" encoding="utf-8"?>
<animated-selector xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="#+id/open"
android:drawable="#drawable/ic_drawer_closed"
android:state_selected="true"/>
<item
android:id="#+id/closed"
android:drawable="#drawable/ic_drawer"/>
<transition
android:drawable="#drawable/avd_drawer_close"
android:fromId="#id/open"
android:toId="#id/closed"/>
<transition
android:drawable="#drawable/avd_drawer_open"
android:fromId="#id/closed"
android:toId="#id/open"/>
</animated-selector>
And here's the animation itself:
You shoud use existing AnimatedVectorDrawable on the internet. Or you should create it with your icons on Shape Shifter web site. I recommend you to watch a tutrial on youtube for this process: ShapeShifter Tutorial.
Handle the state change of the button by setting a state value in the tag of the button and change the Animatable vector drawable resource according to the value of the current solution may also work.
btnMenuView.setOnClickListener {
val state = it.getTag(R.string.meta_tag_menu_button_state).toString().trim()
context?.let {
var animDrawable =
AnimatedVectorDrawableCompat.create(it, R.drawable.avd_drawer_open)
if (state != "open") {
animDrawable =
AnimatedVectorDrawableCompat.create(it, R.drawable.avd_drawer_close)
btnMenuView.setTag(R.string.meta_tag_menu_button_state, "open")
} else {
btnMenuView.setTag(R.string.meta_tag_menu_button_state, "close")
}
btnMenuView.setImageDrawable(animDrawable)
val animatable: Drawable? = btnMenuView.drawable
if (animatable is Animatable) {
animatable.start()
}
}
---Rest of your code
}