I have a bunch of drawables in a custom view. I want the user to be able to press on one or multiple drawables and it changes colors. Currently, each drawable is just a StateListDrawable with two states: state_pressed and not pressed. Every time I press a drawable, setState returns true so I'm assuming that it is actually changed, but I don't see the drawable image change. Is invalidateDrawable not doing anything? What am I doing wrong? How can I redraw the one drawable when pressed without needing call customView.invalidate() and redrawing the whole thing each time? I was doing that originally but found that my app ran very slowly/inefficiently. Thanks!
The flow:
Custom View (contains set of our custom class - TouchKey)
- Custom class TouchKey containing drawable and info
- Upon press or release, custom class finds which drawable to change
Here's code for a button touch within TouchKey class (MyTouch is a custom class tracking all the touches on the android device):
public void pressed(MyTouch touch) {
boolean successfulStateChange = this.drawable.setState(new int[]{android.
R.attr.state_pressed});
this.customView.invalidateDrawable(drawable);
}
public void released(MyTouch touch) {
boolean successfulStateChange = this.drawable.setState(new int[]{-android.
R.attr.state_pressed});
this.customView.invalidateDrawable(drawable);
}
How my StateListDrawable is being drawn in my custom view:
public class CustomView extends View {
private TreeMap<Integer, TouchKey> keymap;
/* Initialization Code Stuff Here - call drawKey */
// StateListDrawable Creation
private StateListDrawable drawKey(Canvas canvas, int bounds_l,
int bounds_t, int bounds_r, int bounds_b)
throws Resources.NotFoundException, XmlPullParserException, IOException {
StateListDrawable key = new StateListDrawable();
key.addState(new int[]{android.R.attr.state_pressed},
ContextCompat.getDrawable(mContext, R.drawable.key_pressed));
key.addState(new int[]{-android.R.attr.state_pressed},
ContextCompat.getDrawable(mContext, R.drawable.key_released));
key.setBounds(bound_l, bounds_t, bounds_r, bounds_b);
key.draw(canvas);
return key;
}
}
I was doing that originally but found that my app ran very slowly/inefficiently
If do it so you have a big advantages in some place in your code, or (I suppose) doesn't scale images before drawing. So try to find a logic on your code that have a huge advantage on system. Because View.invalidate() so fast method.
Other 0,02$ :
I suppose that you develop something like a keyboard. For this case you need invalidate just region of your canvas.
View.invalidate(new Rect(0, 0, 49, 49));
I had the problem too, but I solve it in the end. The reason for this problem is that the Drawable object which you are using in the context doesn't setup it's Bounds by call setBounds(Rect), so it's Bounds is Rect(0,0,0,0) by default. This cause invalidateDrawable() of View which Drawable attached not working.
See the View.invalidateDrawable():
#Override
public void invalidateDrawable(#NonNull Drawable drawable) {
if (verifyDrawable(drawable)) {
final Rect dirty = drawable.getDirtyBounds();
final int scrollX = mScrollX;
final int scrollY = mScrollY;
invalidate(dirty.left + scrollX, dirty.top + scrollY,
dirty.right + scrollX, dirty.bottom + scrollY);
rebuildOutline();
}
}
Look check Drawable.getDirtyBounds():
/**
* Return the drawable's dirty bounds Rect. Note: for efficiency, the
* returned object may be the same object stored in the drawable (though
* this is not guaranteed).
* <p>
* By default, this returns the full drawable bounds. Custom drawables may
* override this method to perform more precise invalidation.
*
* #return The dirty bounds of this drawable
*/
#NonNull
public Rect getDirtyBounds() {
return getBounds();
}
So the Rect dirty is Rect(0,0,0,0), if you not setup Drawable.Bounds.Therefore View.invalidate() doesn't working.
So what you have to do is setup Drawable.Bounds in some place in the code,like that:
#Override
public void draw(#NonNull Canvas canvas) {
Rect localRect = canvas.getClipBounds();
this.setBounds(localRect);
...
}
Related
I'm working in an app that uses the ml kit text recognition library; The app reads text from images and puts a Rect around every word.
Now I want these Rects to change color when the user tap on one of them or swipe above some words.
So, I was able to handle touch events correctly, but what I can't do is changing the color of the touched Rect!
Should I draw new colored Rects above the touched Rect? Or can I color the existing rects (which I don't think I can)?
Classes:
TextGraphic, GraphicOverlay.
//This is where the Rects get drawn
I also tried this solution, so I typed this methods in the TextGraphic class:
public void changeColor(boolean isHighlighted) {
if(isHighlighted) {
rectPaint.setColor(COLOR_HIGHLIGHTED);
rectPaint.setAlpha(400);//set opacity of rect
}else{
rectPaint.setColor(COLOR_DEFAULT);
rectPaint.setAlpha(400);//set opacity of rect
}
postInvalidate();
}
and called it when the user touches the text, but the problem is that all Rects colors get changed, and they do not change at runtime!
A snippet from my ActivityClass, where I used some callback methods to pass things out.
ArrayList<Rect> rects = new ArrayList<>();
#Override
public void onAdd(FirebaseVisionText.Element element, Rect elementRect, String wordText) {
GraphicOverlay.Graphic textGraphic = new TextGraphic(mGraphicOverlay, element);
mTextGraphic = new TextGraphic(mGraphicOverlay, element);
mGraphicOverlay.add(textGraphic);
rects.add(elementRect);
}
A snippet from my ActivityClass where I handle touch events:
#Override
public boolean onDown(MotionEvent event) {
helper.dismissKeyboard();
touchX = Math.round(event.getX());
touchY = Math.round(event.getY());
for(int x=0; x< rects.size();x++) {
if (rects.get(x).contains(touchX, touchY)) {
// line has been clicked
mTextGraphic.changeColor(true);
return true;
}
}
return true;
}
You are changing the color using the mTextGraphic variable. If you look closely in your onAdd() method you will see that you are assigning a new object to mTextGraphic that has nothing to do with the objects drawn to screen because only the objects that you add to GraphicOverlay list using the mGraphicOverlay.add() will get drawn to screen.
So what you need is to call changeColor() not on mTextGraphic but on the respective object that is already in the list inside GraphicOverlay
Since the list inside GraphicOverlay is private you can't manipulate it in the onDown() method. You will need to write a public method that will do the job for you.
Write the following method in GraphicOverlay class
public TextGraphic getGraphicElementAtIndex(int index) {
return (TextGraphic)graphics.get(index)
}
Now use this method inside the if condition of onDown() method like this
if (rects.get(x).contains(touchX, touchY)) {
// line has been clicked
Log.d("PreviewActivity", "changeColor() is called");
mGraphicOverlay.getGraphicElementAtIndex(x).changeColor();
touchedText = texts.get(x);
return true;
}
Hope this helps.
SIDE NOTE: Now even after this if for some reason the ordering of objects inside rects list and graphics list (which is inside GraphicOverlay) change then you will see that when you click a rectangle some other rectangle changes color.
Maybe you should not do it by coding but by ColorStateList
Android Developer: colorstatelist
I use these two methods in my project, but two values sometimes equal, sometimes not.
public class HintView extends LinearLayout {
#Override
protected void dispatchDraw(Canvas canvas) {
this.getWidth();
canvas.getWidth();
}
}
If you inspect the source code for both LinearLayout and Canvas, you'd notice:
LinearLayout's getWidth() method is actually from View.java, a class LinearLayout extends:
* Return the width of your view.
*
* #return The width of your view, in pixels.
*/
#ViewDebug.ExportedProperty(category = "layout")
public final int getWidth() {
return mRight - mLeft;
}
If you on the other hand check out the Canvas' method:
/**
* Returns the width of the current drawing layer
*
* #return the width of the current drawing layer
*/
public int getWidth() {
return nGetWidth(mNativeCanvasWrapper);
}
The n prefix is Google's way to saying: this goes to native (c++) code.
The view and its canvas can and will be different if the view has paddings, margins, etc. (i don't recall of off the top of my head all the related properties) but remember that a Canvas is just a "surface" with some given dimensions, contained in a View.
All things on Screen on Android are one form of View or another. A view needs to measure, and ask its children (if a ViewGroup, which is also a View), and then, after all this is done, it can confidently start drawing (onDraw(Canvas)).
I have created a custom view class to use in a project I'm working on. To put is simply, I'm displaying an image, then adding images on top of the original image (currently by clicking on the image, but that's not final).
I have these 2 methods in my custom view:
protected void onDraw(Canvas canvas)
{
super.onDraw(canvas);
for (Drawable d : drawableList)
{
d.draw(canvas);
}
}
public void AddPoint(float x, float y)
{
Drawable tempDrawable = pin;
tempDrawable.setBounds((int)x, (int)y, (int)x + 50, (int)y + 50);
drawableList.add(tempDrawable);
invalidate();
}
The AddPoint() method is called in an OnTouchListener and is passes the coordinates of the touch event.
The way it currently works is it displays the main image, but will only display the most recent images where I clicked, previous ones just disappear.
Does anybody know what I am doing wrong here?
I've figured out what I was doing wrong.
The line of code:
Drawable tempDrawable = pin;
I changed to:
Drawable tempDrawable = mainRes.getDrawable(R.drawable.pin);
I got mainRes from the init(Context context) method I wrote in the custom View class.
I want to divide the image into sub images and when I click on a part of the image will give me the name of the region, for example, this is my question how to recognize a region from the image, or how to divide the image into sub-images and use it in imageViews
And thank you in advance
In my opinion #fractalwrench's idea is quite good for your case. Basic steps are listed below.
Subclass Android ImageView. For example, MultiRegionImageView.
Override its onTouchEvent method. (This method gets called whenever user touches the view)
User touches the image and thereby onTouchEvent is called and provides the exact touch point (x, y).
Declare another method or interface which determines at which region a given point is. For example, getRegionByPoint(int x, int y)
If you would like to highlight that region boundaries, you could use paths. First off, you should define paths and save them into a raw file (XML, for example), then using region ID, fetch its path and finally draw that path over the main image.
For drawing a path over the main image, you should also override onDraw method of ImageView class and use canvas.drawPath();
public class MultiRegionImageView extends ImageView {
RegionProvider mRegionProvider;
int mId = -1;
private Paint mPaint;
public MultiRegionImageView(Context context) {
super(context);
}
#Override
public boolean onTouchEvent(MotionEvent event) {
mId = mRegionProvider.getRegionIdByPoint(event.getX(), event.getY());
return super.onTouchEvent(event);
}
#Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if(mId != -1) {
canvas.drawPath(mRegionProvider.getRegionBoundaryPath(mId), mPaint);
}
}
public interface RegionProvider{
int getRegionIdByPoint(float x, float y);
Path getRegionBoundaryPath(int id);
}
}
You should only need one ImageView to display the map. If you override onTouchEvent(MotionEvent e), you can get the position which is being touched in the View. If you store the position and shape of each region in some sort of List, you can check whether a touch event is within a region (and display whatever text you need to).
I've been going through all of the documentation, and examples found on the web, but I have yet to find any good resources on building custom views using Java ONLY. I have seen many that describe about creating them using XML and Java.
I think the thing that I'm struggling with the most are relatively simple things like, "can I use a "TextView" widget in a custom view AND also use the canvas to draw some graphics? I've been successful doing one or the other but can't seem to find ways to do both simultaneously.
For example: I would like to have an TextView widget at the top of a view with a touchable graphic underneath that acts as a dial and is drawn onto the canvas which takes up all available space once the TextView is added. As I turn the dial, it increments text inside the TextView. I would think something like the following would work... but it doesn't and it most likely because I'm missing something simple.
public class Dial extends LinearLayout implements View.OnTouchListener
{
Bitmap _dial;
protected int bpm = 120;
TextView txtBpm;
/**
* Constructor
* #param context
*/
public Dial(Context context)
{
super(context);
_dial = BitmapFactory.decodeResource(getResources(), R.drawable.dial_small);
txtBpm = new TextView(context);
txtBpm.setTextSize(43);
txtBpm.setWidth(100);
txtBpm.setText(String.valueOf(bpm));
this.addView(txtBpm);
}
#Override
protected void onDraw(Canvas canvas)
{
super.onDraw(canvas);
// Setting the y-pos to 160 so even
// if the canvas fills the view,
// the dial will still be below the textview.
canvas.drawBitmap(_dial, 0,160,null);
}
#Override
public void onSizeChanged(int w, int h, int oldW, int oldH)
{
}
/* (non-Javadoc)
* #see android.view.View.OnTouchListener#onTouch(android.view.View, android.view.MotionEvent)
*/
#Override
public boolean onTouch(View arg0, MotionEvent arg1)
{
// Do some stuff here to rotate the dial
return false;
}
}