I know we can draw a vector drawable like this on canvas:
vectorDrawable.setBounds(left, top, right, bottom);
vectorDrawable.draw(canvas);
but Is it possible to use animatedVectorDrawables in custom view?
Yes, it is possible, and looks like the snippet you have would work. Be sure to set bounds and call the start() method on the drawable to start the animation.
class MyCustomView : View {
lateinit var animVectDrawable: AnimatedVectorDrawable
fun startAnim() {
animVectDrawable.setBounds(left, top, right, bottom)
animVectDrawable.start()
shouldDrawAnim = true
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
if (shouldDrawAnim) {
animVectDrawable.draw(canvas)
}
}
}
Related
class DrawingView(context: Context, attributeSet: AttributeSet) : View(context, attributeSet) {
private lateinit var mDrawPath: FingerPath
private lateinit var mCanvasBitmap: Bitmap
private lateinit var mCanvasPaint: Paint
private lateinit var mDrawPaint: Paint
private var mBrushSize = 0
private var color = Color.BLACK
private lateinit var canvas: Canvas
init {
setUpDrawing()
}
private fun setUpDrawing() {
mDrawPaint = Paint()
mDrawPath = FingerPath(color, mBrushSize.toFloat())
mDrawPaint.color = color
mDrawPaint.style = Paint.Style.STROKE
mDrawPaint.strokeJoin = Paint.Join.ROUND
mDrawPaint.strokeCap = Paint.Cap.ROUND
mCanvasPaint = Paint(Paint.DITHER_FLAG)
mBrushSize = 20
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
mCanvasBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888)
canvas = Canvas(mCanvasBitmap)
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
canvas.drawBitmap(mCanvasBitmap, 0f, 0f, mDrawPaint)
if (!mDrawPath.isEmpty) {
mDrawPaint.strokeWidth = mDrawPath.brushThickness
mDrawPaint.color = mDrawPath.color
canvas.drawPath(mDrawPath, mDrawPaint)
}
}
override fun onTouchEvent(event: MotionEvent?): Boolean {
val touchX = event?.x
val touchY = event?.y
when (event?.action) {
MotionEvent.ACTION_DOWN -> {
mDrawPath.color = color
mDrawPath.brushThickness = mBrushSize.toFloat()
mDrawPath.reset()
mDrawPath.moveTo(touchX!!, touchY!!)
}
MotionEvent.ACTION_MOVE -> {
mDrawPath.lineTo(touchX!!, touchY!!)
}
MotionEvent.ACTION_UP -> {
mDrawPath = FingerPath(color, mBrushSize.toFloat())
}
else -> return false
}
invalidate()
return true
}
internal inner class FingerPath(var color: Int, var brushThickness: Float) : Path()
}
So, I'm taking a course about Android Development and the instructor is building a drawing app -- and I can't understand how to code works, because he's not explaining the 'why'; so, he types a lot of code without explaining why he uses that variable or why is he overring those functions; therefore, I don't understand how to code work. Could you help me in understanding how this code work?
class DrawingView(context: Context, attributeSet: AttributeSet) : View(context, attributeSet) {
private lateinit var mDrawPath: FingerPath
private lateinit var mCanvasBitmap: Bitmap
private lateinit var mCanvasPaint: Paint
private lateinit var mDrawPaint: Paint
private var mBrushSize = 0
private var color = Color.BLACK
private lateinit var canvas: Canvas
init {
// init block will called first when instance will be created so we
// are calling method setUpDrawing() as it is initialising everything
// that required to draw like color , brush size , brush behaviour
// (round , stroke etc .. ) . in simple manner , we can say painter is
// collecting all required tools before starting to paint
setUpDrawing()
}
private fun setUpDrawing() {
mDrawPaint = Paint()
mDrawPath = FingerPath(color, mBrushSize.toFloat())
mDrawPaint.color = color
mDrawPaint.style = Paint.Style.STROKE
mDrawPaint.strokeJoin = Paint.Join.ROUND
mDrawPaint.strokeCap = Paint.Cap.ROUND
mCanvasPaint = Paint(Paint.DITHER_FLAG)
mBrushSize = 20
}
// this method is going to be called by system when size is going to be
// changed so we are here creating blank board on which we are going to
// draw
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
mCanvasBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888)
canvas = Canvas(mCanvasBitmap)
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
canvas.drawBitmap(mCanvasBitmap, 0f, 0f, mDrawPaint)
if (!mDrawPath.isEmpty) {
mDrawPaint.strokeWidth = mDrawPath.brushThickness
mDrawPaint.color = mDrawPath.color
canvas.drawPath(mDrawPath, mDrawPaint) // drawing path on canvas
}
}
// this method will be called by system when user is going to touch screen
override fun onTouchEvent(event: MotionEvent?): Boolean {
val touchX = event?.x
val touchY = event?.y
when (event?.action) {
// this event will be fired when user put finger on screen
MotionEvent.ACTION_DOWN -> {
mDrawPath.color = color
mDrawPath.brushThickness = mBrushSize.toFloat()
mDrawPath.reset() // reseting path before we set inital point
mDrawPath.moveTo(touchX!!, touchY!!)// set point from where drawing will be started
}
// this event will be fired when user start to move it's fingure . this will be fired continually until user pickup fingure
MotionEvent.ACTION_MOVE -> {
mDrawPath.lineTo(touchX!!, touchY!!)
}
// this event will be fired when user will pick up fingure from screen
MotionEvent.ACTION_UP -> {
mDrawPath = FingerPath(color, mBrushSize.toFloat())
}
else -> return false
}
invalidate() / /refreshing layout to reflect drawing changes
return true
}
internal inner class FingerPath(var color: Int, var brushThickness: Float) : Path()
}
This code is a custom view class in Android for drawing. The class extends the View class and implements touch event handling to allow a user to draw on the screen.
The class defines instance variables to store the paint, path, bitmap and canvas used for drawing. It also has variables for brush size and color.
In the setUpDrawing method, the paint object for drawing and the path object for tracking the user's touch are set up.
In onSizeChanged, the bitmap object is created and its canvas object is initialized.
In onDraw, the canvas is drawn on the screen and the path is drawn on the canvas if it's not empty.
The onTouchEvent method handles touch events (DOWN, MOVE, UP) and updates the path accordingly.
The FingerPath inner class extends the Path class and contains the color and brush size of the path.
Have you read through this section of the docs? Custom View Components
It explains the basics of creating a custom view, which on a basic level means overriding onDraw (where you draw the contents of the view) and maybe onSizeChanged (where you're told how big the view is - maybe you want to set the thickness of your paint brushes, work out where the centre of the view is etc).
onTouchEvent is a little more advanced, that allows you to handle the user poking at your view - the link up there gets into it, but you'll have a better understanding if you read the section on touch gestures. Broadly, you can get info about interaction events through onTouchEvent and work out things like whether the user touched the view, moved a finger, lifted it or moved it out of bounds etc. You'd use this info to, say, draw stuff under the user's finger for a painting app.
A Canvas is a drawing interface for a Bitmap. Instead of having to work out what pixels to change to what colour to draw a line or whatever, you do all your drawing through a Canvas that wraps that Bitmap. This lets you abstract the process and do high-level stuff like "draw a line", "draw an arc", "draw this entire path", "render this text" and so on. The Canvas works out how to make that happen in the Bitmap's pixel array.
The drawing happens with Paints, which you can think of as an object that says how the drawing happens. What does drawing a line look like? The Paint you use defines things like stroke width, transparency, colour etc - it's like the tool options in a paint program. It also allows you to set things like text size (if you're using it to render text) and do fancy things with blending over the pixels it's being drawn on, create shaders etc. It can also do things like control tiling if you're using a bitmap as the source image.
Generally you'll want to create these Paints at startup and reuse them during your View's lifespan, so it's typical to have a few of them - the one for painting the background, the one for drawing curves, the one for rendering text etc.
Usually in onDraw you just draw the view in its current state. The method takes a Canvas parameter - that's how you draw to the actual bitmap rendered on the UI. So if you had a knob, you could draw the circle, draw the position indicator at the appropriate location, add any text, and you're done!
For a drawing app it's a little more complicated - that "state" you need to remember and draw is the sum of all the drawing operations that have happened. You can get fancy with this (to create levels of undo) but the simplest approach is to create your own Bitmap to store the current drawing. You wrap that in its own Canvas (so you can draw on it) and then all the user interaction draws stuff to that Canvas, so you're painting on the bitmap. Then, in onDraw, you basically just need to paint the contents of that bitmap onto the system-provided Canvas so it gets drawn on-screen - just showing what you have, basically.
The code you have here is a little weird - you have that internal bitmap (called mCanvasBitmap) wrapped in a Canvas (called canvas) but you never actually draw to it. I feel like this line is meant to be rendering the Path you build up with the touch gestures:
override fun onDraw(canvas: Canvas) {
...
if (!mDrawPath.isEmpty) {
...
canvas.drawPath(mDrawPath, mDrawPaint) // drawing path on canvas
}
}
but in onDraw's scope canvas is referring to that parameter being passed in, i.e. the Canvas for drawing to the screen, not the one wrapping mCanvasBitmap. So that internal bitmap always stays empty, and then you just draw the most recent path (this is a good example of watching how you name things - why not mCanvas to be consistent?). It never "builds up" an image by drawing paths onto your internal bitmap. I can't tell what the exact intent is from the code though, I just thought I'd point it out!
I am making a simple Loading spinner view component that has two image assets. The first image is a spinner that should show and rotate while the component is "loading". The second image is a checkmark that should show without any animation when the "loading" has finished. I'd like to use one ImageView and just swap out the image asset when needed.
Here's the spinner image:
And here's the checkmark image:
I wrote a function to stop the animation and replace the image but when I call it this is what I see:
It looks like the image is getting changed but then the original image is getting redrawn.
Here's my function that starts the animation:
fun show() {
rotateAnimation = RotateAnimation(0f, 360f, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f)
rotateAnimation!!.duration = 900
rotateAnimation!!.repeatCount = Animation.INFINITE
rotateAnimation!!.fillAfter = true
mLoaderView.startAnimation(rotateAnimation)
}
And here's my function that stops the animation and changes the image:
private fun completeLoading() {
rotateAnimation!!.setAnimationListener(object : Animation.AnimationListener {
override fun onAnimationStart(p0: Animation?) {
// not implemented
}
override fun onAnimationRepeat(p0: Animation?) {
// not implemented
}
override fun onAnimationEnd(p0: Animation?) {
mLoaderView.setBackgroundDrawable(Util.getLocalDrawable(mLoaderView.context, "icn_loading_error"))
}
})
mLoaderView.clearAnimation()
}
How can I prevent the spinner image from getting drawn over the checkmark image?
I'm making android app with Kotlin in android studio now, and want to draw on already drawn custom view.
This app is simple app about shows graphical information with received data with serial port. Actually I'm new on Kotlin and rookie on Java/Android. This is code that has problem.
This is XML of custom view, (it is in the contraintlayout)
<!--activity_main.xml-->
<androidx.constraintlayout.widget.ConstraintLayout ... >
<com.(...).DrawUI
android:id="#+id/DrawUI" ...
/>
Trying to call drawing function 'DrawRxData(rxData)' on this thread,
//BluetoothController.kt
class BluetoothClient(private val activity: MainActivity,
private val socket:BluetoothSocket): Thread() {
override fun run() {
...
if(inputStream.available() > 0) {
...
inputStream.read(buffer)
val rxData = BluetoothRxParser(buffer)
DrawRxData(rxData)
...
}
...
}
}
And this is DrawUI that wrote above on XML
I want to draw something on this View with calling function in BluetoothController.kt!
//DrawUI.kt
class DrawUI(context: Context, attrs: AttributeSet?) : View(context, attrs) {
init {
// initializing Paint()s
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
// measuring dimension and determine drawing sector
}
override fun onDraw(canvas: Canvas) {
// drawing background
}
}
I tried use Canvas on DrawUI class but it didn't work properly.
Also tried making Bitmap but it occurs error that width and height are zero. How can I fix this problem?
EDIT
This way is how I solved this problem, maybe there is another and simple solution..
Created ImageView that has same size with DrawUI on XML
<ImageView
android:id="#+id/DrawRx" ...
/>
Get View and cast to ImageView, then create bitmap and canvas.. DrawRxData() is in BluetoothClient class.
private fun DrawRxData (rxData: RxData) {
val v : View = activity.findViewById(R.id.DrawRx)
val iv : ImageView = v as ImageView
val bitmap = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888)
val canvas = Canvas(bitmap)
val paint = Paint(Paint.ANTI_ALIAS_FLAG)
paint.setColor(Color.BLACK)
canvas.drawCircle(50F, 50F, 10F, paint)
Attach bitmap to ImageView but for thread, it looks like Dispatcher in C#..
Reference
activity.runOnUiThread(java.lang.Runnable {
iv.setImageBitmap(bitmap)
})
Actually just overlapped exist view, please answer if there is efficient way!
I read that is bad practice to call invalidate() inside the onDraw method so i want to be able to call invalidate() from my Ball class to the onDraw method that is on the Game class.
In another post i read that creating an object of the Game class and then calling that object like gameClass.invalidate() is the best way to do it and im trying to figure out how to do that.
Im having trouble passing the context and attrs.
Here is my CustomView class:
class Game(context: Context?, attrs: AttributeSet?) : View(context,
attrs){
private val ball1 = Ball(width/2 - 50.toDouble(),150.0,20.0)
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
canvas.apply {
drawOval((width / 2) - 50,
ball1.posy.toFloat() - 50f,
(width / 2) + 50,
ball1.posy.toFloat() + 50f,
circleColor)
}
ball1.update()
}
Here is the class where i need to call invalidate:
is there a better way to call invalidate() here
val gameClass = Game( how do i pass the context and attrs here? )
class Ball(var posx: Double, var posy:Double,var velocity: Double){
//how do i pass the context and attrs here?
val gameClass = Game(...)
fun update(){
posy += 10
gameClass.invalidate()
}
}
here is my xml
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<view android:layout_width="0dp"
android:layout_height="0dp"
class="com.example.myapplication.Game"
id="#+id/view4"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
Okay first things first. It maybe a typo in your question but according to your code Game class is instantiating a Ball object and Ball class is instantiating a Game object. This is called circular dependency and you don't want this because it will result in StackOverFlow error (Ironic isn't it ?). So remove the Game object from Ball class.
Secondly, since you are calling ball1.update() inside onDraw() so calling invalidate() inside ball1.update() is no different than calling it after ball1.update(). In both cases you are calling invalidate() inside onDraw().
The purpose of invalidate() is to tell the view to redraw itself whenever we change any data related to the view. Since you are changing the data related to view inside onDraw() by calling ball1.update() so calling invalidate() right after it would be the logical step to take. Like this and it will work in your case.
class Game(context: Context, attributes: AttributeSet): View(context,attributes) {
private val paint :Paint = Paint(ANTI_ALIAS_FLAG)
private val ball1 = Ball(width/2 - 50.toDouble(),150.0,20.0)
init{
paint.color = Color.CYAN
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
canvas.apply {
drawOval(
(width / 2) - 50f,
ball1.posy.toFloat() - 50f,
(width / 2) + 50f,
ball1.posy.toFloat() + 50f,
paint)
ball1.update()
invalidate()
}
}
}
But onDraw() is not the best place to call invalidate(). There are many performance related issues so it is better to leave onDraw() alone.
Read here:
https://developer.android.com/training/custom-views/optimizing-view
https://www.youtube.com/watch?v=zK2i7ivzK7M
I'm assuming you want to animate your view. So it will be better to read the docs on animation.
But anyway let's continue. If you want to avoid calling invalidate() from onDraw() but still want to achieve the same result. Your best bet would be to create another function inside Game class which will start the process of updating your Ball object and calling invalidate() continuously. I'm naming this method as startAnimation()
This is how we can do this. Since your view is running on the UI Thread we need to get a Handler to it and tell the UI to run a Runnable continuously by using Handler.post(). That Runnable will contain the code that will update your Ball object and call invalidate(). Luckily the View class itself contains post() method which is the same as Handler.post() so we can safely call this method inside our Game class because Game inherits View.
Here is the code :
class Game(context: Context, attributes: AttributeSet): View(context,attributes) {
private lateinit var runnable : Runnable // reference to the runnable object
private val ball1 = Ball(width/2 - 50.toDouble(),150.0,20.0)
private val paint :Paint = Paint(ANTI_ALIAS_FLAG)
//This is the constructor of the class
init{
paint.color = Color.CYAN
//Here we initialize our Runnable and put the code that we want to run once we call startAnimation()
runnable = Runnable {
ball1.update()
invalidate()
//Calling post() inside here will loop the above code and you will see a smooth animation
post(runnable)
}
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
canvas.apply {
drawOval(
(width / 2) - 50f,
ball1.posy.toFloat() - 50f,
(width / 2) + 50f,
ball1.posy.toFloat() + 50f,
paint)
}
}
//This is the new function I am talking about
fun startAnimation()
{
post(runnable)
}
fun stopAnimation()
{
removeCallbacks(runnable)
}
}
And we start the animation by calling startAnimation() from MainActivity
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val game = findViewById<Game>(R.id.view)
game.startAnimation()
}
}
To stop the animation just call stopAnimation()
Read more about processes, threads and handler here :
https://developer.android.com/guide/components/processes-and-threads
https://developer.android.com/reference/android/os/Handler
I set animateLayoutChanges=true in my code, to animate the options item I have in my fragments.
When I open the PreferenceFragment I enable the up navigation icon, then I disable it when I close it, but it behaves in a strange way, leaving a sort of padding to the left of the title, where the NavigationIcon was.
Here's a gif showing what's happening:
Do you guy's have any idea on why is this happening?
I searched far and wide on the internet, but found nothing.
Is there any workaround to animate these items in the same way?
Thanks.
Remark: I know this thread in more than a year long but a lot of people still stumble on this problem.
It took me good few hours to finally dig into this problem and solution is actually relatively easy.
The Scenario
When you call ActionBar.setDisplayHomeAsUpEnabled() or Toolbar.setNavigationIcon(), Toolbar will dynamically add or remove navigation view depending on specified value
public void setNavigationIcon(#Nullable Drawable icon) {
if (icon != null) {
ensureNavButtonView();
if (!isChildOrHidden(mNavButtonView)) {
addSystemView(mNavButtonView, true);
}
} else if (mNavButtonView != null && isChildOrHidden(mNavButtonView)) {
removeView(mNavButtonView);
mHiddenViews.remove(mNavButtonView);
}
if (mNavButtonView != null) {
mNavButtonView.setImageDrawable(icon);
}
}
And in onLayout it will check whether navigation button is present and lay it out.
protected void onLayout(boolean changed, int l, int t, int r, int b) {
// more code before
if (shouldLayout(mNavButtonView)) {
if (isRtl) {
right = layoutChildRight(mNavButtonView, right, collapsingMargins,
alignmentHeight);
} else {
left = layoutChildLeft(mNavButtonView, left, collapsingMargins,
alignmentHeight);
}
}
// more code after. also mTitleTextView is laid out here
}
Toolbar also increments left/right position after laying out each view. These values used as an offset for each subsequent view.
To check whether view should be laid out it uses helper function:
private boolean shouldLayout(View view) {
return view != null && view.getParent() == this && view.getVisibility() != GONE;
}
The Problem
When setting layoutTransition through the code or animateLayoutChanges in the xml we change behaviour a little bit.
Lets start with what happening when we remove child from parent (ViewGroup code):
private void removeFromArray(int index) {
final View[] children = mChildren;
if (!(mTransitioningViews != null && mTransitioningViews.contains(children[index]))) {
children[index].mParent = null;
}
// more code after
}
Did you mention that child's parent is not nulled when we have transition set? This is important!
Also please recall what shouldLayout from above does - it checks for only three cases:
view is not null
view's parent is toolbar
view is not GONE
And here is the root of the problem - navigation button is removed from the toolbar but it doesn't event know about this.
The Solution
In order to fix this problem we need to force toolbar into thinking that view shouldn't be laid out. Ideally it would be nice to override shouldLayout method but we don't have such luxury... So the only way we can accomplish this is by changing visibility on the navigation button.
class FixedToolbar(context: Context, attrs: AttributeSet?) : MaterialToolbar(context, attrs) {
companion object {
private val navButtonViewField = Toolbar::class.java.getDeclaredField("mNavButtonView")
.also { it.isAccessible = true }
}
override fun setNavigationIcon(icon: Drawable?) {
super.setNavigationIcon(icon)
(navButtonViewField.get(this) as? View)?.isGone = (icon == null)
}
}
Now toolbar will know that navigation button must not be laid out and all animations will go as intended.
I found a workaround to get the transition to work.
After calling supportActionBar?.setDisplayHomeAsUpEnabled(false) to hide the back arrow button, call the TransitionManager.beginDelayedTransition() method to trigger the transition manually.
// Hide the back arrow button
supportActionBar?.setDisplayHomeAsUpEnabled(false)
// Add a transition listener to the toolbar to set the toolbar title later
mToolbar.layoutTransition.addTransitionListener(object : LayoutTransition.TransitionListener {
override fun startTransition(transition: LayoutTransition?, container: ViewGroup?, view: View?, transitionType: Int) {}
override fun endTransition(transition: LayoutTransition?, container: ViewGroup?, view: View?, transitionType: Int) {
setToolbarTitle("Agenda")
mToolbar.layoutTransition.removeTransitionListener(this)
}
})
TransitionManager.beginDelayedTransition(mToolbar)
BEFORE
AFTER
Even with this questions more than a year back, this issue is still happening with the version androidx.navigation:navigation-ui-ktx:2.4.2
According to #MatrixDev, yes, this is still the final solution is:
class FixedToolbar(context: Context, attrs: AttributeSet?) : MaterialToolbar(context, attrs) {
companion object {
private val navButtonViewField = Toolbar::class.java.getDeclaredField("mNavButtonView")
.also { it.isAccessible = true }
}
override fun setNavigationIcon(icon: Drawable?) {
super.setNavigationIcon(icon)
(navButtonViewField.get(this) as? View)?.isGone = (icon == null)
}
}