I have created a custom view that only extends the View class. The custom view works perfectly, except when being used inside a RecyclerView. This is the custom view:
public class KdaBar extends View {
private int mKillCount, mDeathCount, mAssistCount;
private int mKillColor, mDeathColor, mAssistColor;
private int mViewWidth, mViewHeight;
private Paint mKillBarPaint, mDeathBarPaint, mAssistBarPaint, mBgPaint;
private float mKillPart, mDeathPart, mAssistPart;
public KdaBar(Context context, AttributeSet attrs) {
super(context, attrs);
TypedArray a = context.getTheme().obtainStyledAttributes(
attrs,
R.styleable.KdaBar,
0, 0);
try {
mKillCount = a.getInt(R.styleable.KdaBar_killCount, 0);
mDeathCount = a.getInt(R.styleable.KdaBar_deathCount, 0);
mAssistCount = a.getInt(R.styleable.KdaBar_assistCount, 0);
mKillColor = a.getColor(R.styleable.KdaBar_killBarColor, ContextCompat.getColor(getContext(), R.color.kill_score_color));
mDeathColor = a.getColor(R.styleable.KdaBar_deathBarColor, ContextCompat.getColor(getContext(), R.color.death_score_color));
mAssistColor = a.getColor(R.styleable.KdaBar_assistBarColor, ContextCompat.getColor(getContext(), R.color.assist_score_color));
} finally {
a.recycle();
}
init();
}
public void setValues(int killCount, int deathCount, int assistCount) {
mKillCount = killCount;
mDeathCount = deathCount;
mAssistCount = assistCount;
invalidate();
}
#Override
public void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawRect(0f, 0f, mViewWidth, mViewHeight, mBgPaint);
canvas.drawRect(mKillPart+mDeathPart, 0f, mKillPart+mDeathPart+mAssistPart, mViewHeight, mAssistBarPaint);
canvas.drawRect(mKillPart, 0f, mKillPart+mDeathPart, mViewHeight, mDeathBarPaint);
canvas.drawRect(0f, 0f, mKillPart, mViewHeight, mKillBarPaint);
}
#Override
protected void onSizeChanged(int xNew, int yNew, int xOld, int yOld){
super.onSizeChanged(xNew, yNew, xOld, yOld);
mViewWidth = xNew;
mViewHeight = yNew;
float total = mKillCount + mDeathCount + mAssistCount;
mKillPart = (mKillCount/total) * mViewWidth;
mDeathPart = (mDeathCount/total) * mViewWidth;
mAssistPart = (mAssistCount/total) * mViewWidth;
}
private void init() {
mKillBarPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mKillBarPaint.setColor(mKillColor);
mDeathBarPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mDeathBarPaint.setColor(mDeathColor);
mAssistBarPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mAssistBarPaint.setColor(mAssistColor);
mBgPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mBgPaint.setColor(ContextCompat.getColor(getContext(), R.color.transparent));
}
}
The linked image is what the custom view currently looks like (The custom view is the rectangle above the numbers at the center) http://imgur.com/a/Ib5Yl
The numbers below that bar represents their value (They are color-coded in case you haven't noticed). It is obvious that a value of zero on the first item shouldn't show a blue bar on the custom view. Weird, I know.
The method below is where the values are set (it is inside the RecyclerView.Adapter<>):
#Override
public void onBindViewHolder(ViewHolder holder, int position) {
MatchHistory.Match item = mDataset.get(position);
MatchHistory.MatchPlayer[] players = item.getPlayers();
for(MatchHistory.MatchPlayer player: players) {
int steamId32 = (int) Long.parseLong(mCurrentPlayer.getSteamId());
if (steamId32 == player.getAccountId()) {
mCurrentMatchPlayer = player;
}
}
...
holder.mKdaBar.setValues(mCurrentMatchPlayer.getKills(), mCurrentMatchPlayer.getDeaths(), mCurrentMatchPlayer.getAssists());
...
}
This is the onCreateViewHolder:
#Override
public MatchesAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.fragment_match_item, parent, false);
ViewHolder vh = new ViewHolder(v);
return vh;
}
and the ViewHolder class:
public static class ViewHolder extends RecyclerView.ViewHolder {
KdaBar mKdaBar;
public ViewHolder(View v) {
super(v);
...
mKdaBar = (KdaBar) v.findViewById(R.id.kda_bar);
...
}
}
I think it is useful to note that the dataset being used by the adapter changes the position of the items from time to time (since it is being fetched all at the same time but are inserted so that the dataset is ordered). I almost forgot that I also tested not changing the positions of the items inside the dataset, but still there aren't any good results. If you checked the image, you can see that there are other info inside the items and I am 100% sure those are all correct with the exception of the data in the custom view.
I am thinking that I am forgetting some methods that must be overridden but I already saw a lot of tutorials and none of them mentioned about this issue. Looking forward to solving this issue. TIA!
It is pretty hard to tell what is going on exactly especially if this code is working elsewhere, but I'll take a couple guesses.
The main things I noticed:
Comparing int from long where numbers are dangerously close to max
Calling Invalidate from a View inside a RecyclerView (especially onBindView)
Issue 1
In your picture, I'm guessing you are the steamId which are the numbers on the bottom left corner of each RecyclerView's view holder, for example: '2563966339'. You should know that "usually" in Android, Integer.MAX_VALUE = 2147483647. This pretty much means you should use long or things won't be equal when you think they are... (so maybe the boxes are being drawn correctly, but you just don't think the steamId at position 0 is the guy you think?!?!).
(If you want to learn more about it just looked up signed vs usigned bytes for int and long).
So you might have to change some code, but I recommend using long or Long. Two of Many Possibilities Below
Example 1
long steamId32 = Long.parseLong(mCurrentPlayer.getSteamId());
if (steamId32 == player.getAccountId()) {
mCurrentMatchPlayer = player;
}
Example 2
Long steamId32 = mCurrentPlayer.getSteamId();
if (steamId32.equals(player.getAccountId()) {
mCurrentMatchPlayer = player;
}
Issue 2:
A lack of understanding of how RecyclerView works might be causing some problems. In onBindView, you should setup and draw the view as much as possible (without calling invalidate()). This is because RecyclerView is meant to handle all 'recycling'. So you invalidate() call might be causing some strange problems.
I know that onDraw() isn't normally called every time a view is bound, but only upon creation with RecyclerView. This would explain why it worked elsewhere!
Summary and Analysis:
Number 1:
I would call (inside onBindView before setValues)
Log.d("Whatever", "At position: " + position + " we have " + <steamId> + <kills> + <other desired info>).
After you scroll up and down you will see the person on the top and what values are being called and see if it is a problem mentioned in #1 or a problem with your position. If the person should have 0, then let position 0 show 0 kills.
This could also point out one of these problems that I didn't think were as likely, but definitely possible:
I still don't know what mCurrentPlayer is exactly which could cause a problem. Also, if you need to update a 'item' in the adapter simply call mAdapter.updateItemAt(position) from the Activity/Fragment with recyclerView. If you have to move it call mAdapter.notifyItemMoved(fromPos, toPos). All these mean that maybe things aren't what you think when onBindView is being called.
Number 2:
I would recommend putting Log statements also in onDraw() to see if you know when it is ACTUALLY being called, and not just expect it after invalidate(). Most likely invaidate() is being queued by the main thread / recycler view until it decides it wantes to call onDraw().
(Because it already created/drew the item in onCreateView())
You might be surprised by what RecyclerView, LayoutManager, and the Adapter do and how they call the view methods. (You might also just want to put Log statements in onBindView and onCreateView to understand the whole process with onDraw()).
Understanding RecyclerView (and it's parts)
Videos to Learn Basics:
RecyclerView ins and outs - Google I/O 2016
RecyclerView Animations and Behind the Scenes (Android Dev Summit
2015)
And for the readers, Android documentation provided this summary:
Adapter: A subclass of RecyclerView.Adapter responsible for providing views that represent items in a data set.
Position: The position of a data item within an Adapter.
Index: The index of an attached child view as used in a call to getChildAt(int). Contrast with Position.
Binding: The process of preparing a child view to display data corresponding to a position within the adapter.
Recycle (view): A view previously used to display data for a specific adapter position may be placed in a cache for later reuse to display the same type of data again later. This can drastically improve performance by skipping initial layout inflation or construction.
Scrap (view): A child view that has entered into a temporarily detached state during layout. Scrap views may be reused without becoming fully detached from the parent RecyclerView, either unmodified if no rebinding is required or modified by the adapter if the view was considered dirty.
Dirty (view): A child view that must be rebound by the adapter before being displayed.
RecyclerView
RecyclerView.ViewHolder
RecyclerView.LayoutManager
RecyclerView.Adapter
The problem is not with the dataset but with my understanding of how RecyclerView works underneath (just as napkinsterror have mentioned in his answer).
This it the revised custom view:
public class KdaBar extends View {
private int mKillCount, mDeathCount, mAssistCount;
private int mKillColor, mDeathColor, mAssistColor;
private int mViewWidth, mViewHeight;
private Paint mKillBarPaint, mDeathBarPaint, mAssistBarPaint, mBgPaint;
private float mKillPart, mDeathPart, mAssistPart;
public KdaBar(Context context, AttributeSet attrs) {
super(context, attrs);
TypedArray a = context.getTheme().obtainStyledAttributes(
attrs,
R.styleable.KdaBar,
0, 0);
try {
mKillCount = a.getInt(R.styleable.KdaBar_killCount, 0);
mDeathCount = a.getInt(R.styleable.KdaBar_deathCount, 0);
mAssistCount = a.getInt(R.styleable.KdaBar_assistCount, 0);
mKillColor = a.getColor(R.styleable.KdaBar_killBarColor, ContextCompat.getColor(getContext(), R.color.kill_score_color));
mDeathColor = a.getColor(R.styleable.KdaBar_deathBarColor, ContextCompat.getColor(getContext(), R.color.death_score_color));
mAssistColor = a.getColor(R.styleable.KdaBar_assistBarColor, ContextCompat.getColor(getContext(), R.color.assist_score_color));
} finally {
a.recycle();
}
init();
}
public void setValues(int killCount, int deathCount, int assistCount) {
mKillCount = killCount;
mDeathCount = deathCount;
mAssistCount = assistCount;
}
private void calculatePartitions() {
float total = mKillCount + mDeathCount + mAssistCount;
mKillPart = (mKillCount/total) * mViewWidth;
mDeathPart = (mDeathCount/total) * mViewWidth;
mAssistPart = (mAssistCount/total) * mViewWidth;
}
#Override
public void onDraw(Canvas canvas) {
super.onDraw(canvas);
calculatePartitions();
canvas.drawRect(mKillPart+mDeathPart, 0f, mKillPart+mDeathPart+mAssistPart, mViewHeight, mAssistBarPaint);
canvas.drawRect(mKillPart, 0f, mKillPart+mDeathPart, mViewHeight, mDeathBarPaint);
canvas.drawRect(0f, 0f, mKillPart, mViewHeight, mKillBarPaint);
}
#Override
protected void onSizeChanged(int xNew, int yNew, int xOld, int yOld){
super.onSizeChanged(xNew, yNew, xOld, yOld);
mViewWidth = xNew;
mViewHeight = yNew;
}
private void init() {
mKillBarPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mKillBarPaint.setColor(mKillColor);
mDeathBarPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mDeathBarPaint.setColor(mDeathColor);
mAssistBarPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mAssistBarPaint.setColor(mAssistColor);
mBgPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mBgPaint.setColor(ContextCompat.getColor(getContext(), R.color.transparent));
}
}
These are the changes I made:
Removed the invalidate() call from inside the setValues() since the onDraw() callback is invoked when the parent adds a view.
Moved the assignment of mKillPart, mDeathPart, and mAssistPart to calculatePartitions() which is, in turn, called inside onDraw(). This is because the values needed for the calculation are asssured to be complete inside onDraw(). This will be explained below.
This is what I've gathered from Mr. napkinsterror's answer:
When the LayoutManager asks the RecyclerView for a view, ultimately, the onBindViewHolder() method is called. Within that method, data is bound to the views, thus setValues() is called.
The view is returned to the LayoutManager, which will then add the item back to the RecyclerView. This event will trigger onSizeChanged() because the dimensions of the view are not known yet. That's where the mViewWidth and mViewHeight are retrieved. At this point, all the necessary values for calculatePartitions() are complete.
onDraw() is also called because the parent just added an item (check this image). calculatePartitions() is called inside onDraw() and the view will be drawn on the canvas without any problem.
The reason I get wrong values before is because I do the calculatePartitions() inside onSizeChanged() which is very, very wrong since mViewWidth and mViewHeight are yet to be known.
I will mark this as the answer but many thanks to mr. napkinsterror for providing resources so that I can research in the right direction. :)
Related
I'm doing a school project. In this project I have to do a program that have one or more ball bouncing in the screen. I did some research on google to help me in this, and I found this code :
public class BouncingBallInside extends View {
private List<Ball> balls = new ArrayList<>();
public BouncingBallInside(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public BouncingBallInside(Context context) {
super(context);
init();
}
private void init(){
//Add a new ball to the view
balls.add(new Ball(50,50,100, Color.RED));
}
#Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//Draw the balls
for(Ball ball : balls){
//Move first
ball.move(canvas);
//Draw them
canvas.drawOval(ball.oval,ball.paint);
}
invalidate(); // See note
}
}
The ball class :
public class Ball{
public int x,y,size;
public int velX = 10;
public int velY=7;
public Paint paint;
public RectF oval;
public Ball(int x, int y, int size, int color){
this.x = x;
this.y = y;
this.size = size;
this.paint = new Paint();
this.paint.setColor(color);
}
public void move(Canvas canvas) {
this.x += velX;
this.y += velY;
this.oval = new RectF(x-size/2,y-size/2,x+size/2,y+size/2);
//Do we need to bounce next time?
Rect bounds = new Rect();
this.oval.roundOut(bounds); ///store our int bounds
//This is what you're looking for ▼
if(!canvas.getClipBounds().contains(bounds)){
if(this.x-size<0 || this.x+size > canvas.getWidth()){
velX=-velX;
}
if(this.y-size<0 || this.y+size > canvas.getHeight()){
velY=-velY;
}
}
}
}
The program works perfecly.
I studied it deeply as good as I could. But after it and after watching the documentation I couldn't understand two thing:
Where and when the method onDraw(Canvas canvas) is called the first time.
Why at the end of onDraw there is invalidate()?
I mean the documentation said :
Invalidate the whole view. If the view is visible, onDraw(android.graphics.Canvas) will be called at some point in the future.
so... if this method is used to call onDraw,why don't call it direcly? what's the difference?
1)The onDraw method will be called by the framework, whenever the view is invalid. A view is invalid when it first comes on screen, so when you set your content view for an activity they layout and all views in it will be measured, laid out, then drawn (via onDraw).
After that the UI thread will call onDraw if needed every 16ms or so (so it draws at 60 FPS).
2)Its marking the view as needing to be redrawn, so the next time the the screen is drawn onDraw will be called. Otherwise it would be skipped, as we could assume it isn't needed.
Why you don't call onDraw directly- efficiency. In a very simple drawing system you would- but drawing is time consuming, you don't want to do it more than you have to. So instead of drawing immediately (which wouldn't work anyway, you wouldn't have the right Canvas to pass to onDraw), you call invalidate and the system will call onDraw if needed at a regular interval.
Note that this isn't particularly good code. In particular, having the onDraw trigger the move which updates the balls location instead of using a timer is icky. Having onDraw call invalidate as a result is also kind of icky. A better solution would be to separate the view, model, and timer into more of an MVC or MVP system.
I have standard UI elements in rows within a ListView. When I scroll the list too far away from the UI elements and scroll back, they stop responding and the clicks fall through to the cell below.
Any ideas why this might happen?
If I don't scroll away from a UI element, it works. Also, if I scroll up and down a few times, elements will start working again.
It can't be my click handlers failing to fire because a picker will open up - and they don't need client code to open them.
I don't have an adapter. This is via Xamarin. I was hoping someone with some deeper knowledge of Android might have some insight.
I found this article and discovered that rendering a cell can be extremely simple. Then I found the ViewCellRenderer source code in Visual Studio. I copied it, hacked away a load of stuff and the bug has gone.
There was some suspicious stuff regarding handling long presses. Perhaps one day I will go back in and try and find exactly what was causing the problem.
It doesn't seem to have broken so far. I'll update it if it goes wrong.
This is what I'm left with...
public class ResponseViewCellRenderer : ViewCellRenderer
{
protected override Android.Views.View GetCellCore(Cell item, Android.Views.View convertView, ViewGroup parent, Context context)
{
if (convertView is ViewCellContainer container)
{
container.DisposeView();
}
return new ViewCellContainer(context, (ViewCell) item);
}
}
internal class ViewCellContainer : ViewGroup
{
readonly ViewCell viewCell;
readonly IVisualElementRenderer viewRenderer;
public ViewCellContainer(Context context, ViewCell viewCell) : base(context)
{
this.viewCell = viewCell;
viewRenderer = Platform.CreateRendererWithContext(viewCell.View, Context);
Platform.SetRenderer(viewCell.View, viewRenderer);
AddView(viewRenderer.View);
}
protected override void OnLayout(bool changed, int l, int t, int r, int b)
{
var width = Context.FromPixels(r - l);
var height = Context.FromPixels(b - t);
Xamarin.Forms.Layout.LayoutChildIntoBoundingRegion(viewRenderer.Element, new Rectangle(0.0, 0.0, width, height));
viewRenderer.UpdateLayout();
}
protected override void OnMeasure(int widthMeasureSpec, int heightMeasureSpec)
{
var size = MeasureSpec.GetSize(widthMeasureSpec);
var sizeRequest = viewRenderer.Element.Measure(Context.FromPixels(size), double.PositiveInfinity, MeasureFlags.IncludeMargins);
int measuredHeight = (int)Context.ToPixels((viewCell.Height > 0.0) ? viewCell.Height : sizeRequest.Request.Height);
SetMeasuredDimension(size, measuredHeight);
}
public void DisposeView()
{
Platform.SetRenderer(viewCell.View, null);
RemoveView(viewRenderer.View);
viewRenderer.View.Dispose();
}
}
I have a RecyclerView which has a staggeredGridLayoutManager as layout manager. My layout stands as having 2 spans(cols), which items inside may have different heights.
Inflated items has a ImageView and some other views inside a LinearLayout container.
I want to save Inflated(or should I say binded?) View's size(height and width) after the view's image is fully loaded. Because this operation makes me know how much width and height the LinearLayout occupy at final-after the image is placed in the layout-.
After scrolling, this container may be recycled and binded again. What I want to achieve is to savebinded layout's size immediately after it is binded, according to the height and width values previously calculated because this makes recyclerView's item positions more stable. They are less likely move around.
I have mWidth and mHeight members in my ViewHolder, which basically store these values. However, I lost syncronisation between item position in adapter and corresponding ViewHolder. For example I calculate height of 8th item as 380px when it first become visible, which is correct. After recycling and binding 8th position again, my view's height retrieved as 300 px, which is incorrect.
Code:
BasicActivity is derived from Activity..
public ItemsRVAdapter(BasicActivity activity, JSONArray items){
this.items = items;
this.activity = activity;
this.itemControl = new Items(activity);
}
OnCreate:
#Override
public ItemListViewHolders onCreateViewHolder(ViewGroup viewGroup, int i) {
View layoutView =activity.getLayoutInflater().inflate(R.layout.list_element_items, viewGroup, false);
ItemListViewHolders rcv = new ItemListViewHolders(layoutView);
return rcv;
}
OnViewAttachedToWindow (I tried the same code here in different places, like onViewRecycled but I don't know this method is the most right place to calculete the size)
#Override
public void onViewAttachedToWindow(ItemListViewHolders holder)
{
holder.layoutCapsule.measure(LinearLayout.MeasureSpec.makeMeasureSpec(0, LinearLayout.MeasureSpec.UNSPECIFIED), LinearLayout.MeasureSpec.makeMeasureSpec(0, LinearLayout.MeasureSpec.UNSPECIFIED));
if(holder.image.getDrawable() != null){
holder.height = holder.layoutCapsule.getHeight();
holder.width = holder.layoutCapsule.getWidth();
}else{
holder.height = 0;
holder.width = 0;
}
}
onBindViewHolder: Only relevant part. Here I paired position value and my array's member index
#Override
public void onBindViewHolder(ItemListViewHolders holder, int position) {
try {
//JSONObject item = items.getJSONObject(holder.getAdapterPosition());
JSONObject item = items.getJSONObject(position);
holder.image.setImageDrawable(null);
ViewGroup viewGroup = holder.layoutCapsule; //Main Container
...
}
}
I recommend looking for a different approach to resolve your problem with the items moving around not depending on View sizes, but if you want to proceed this way this is my proposed solution:
Don't depend or save the size values on the holder as this gets recycled, you will need to create an object "descriptor" with the values (width and height) for each position and save them on a HashMap or something like that, save the values as you are doing it already, i understand on "onViewAttachedToWindow".
class Descriptor(){
int width;
int height;
void setWidth(int width){
this.width = width;
}
int getWidth(){
return width;
}
void setHeight(int height){
this.height = height;
}
int getHeight(){
return height;
}
Initialize array on constructor:
descriptors = new HashMap<Integer, Descriptor>();
in onBindViewHolder save the position on a view tag to use it on OnViewAttachedToWindow
public void onBindViewHolder(ItemListViewHolders holder, int position) {
....
holder.image.setTag(position);
...
}
populate values on onViewAttachedToWindow
public void onViewAttachedToWindow(ItemListViewHolders holder){
...
int position = (Integer)holder.image.getTag();
Descriptor d = descriptors.get(position);
if(d == null){
d = new Descriptor();
descriptors.put(position, d);
}
d.setWidth(holder.layoutCapsule.getWidth());
d.setHeight(holder.layoutCapsule.getHeight());
...
}
Then use the size data on the descriptor on the method you need getting it by position, you will be creating descriptors as the user is scrolling down, also this works on the asumption that the data maintains the same position during the life of the adapter.
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);
...
}
I am trying to apply an animation to a view in my Android app after my activity is created. To do this, I need to determine the current size of the view, and then set up an animation to scale from the current size to the new size. This part must be done at runtime, since the view scales to different sizes depending on input from the user. My layout is defined in XML.
This seems like an easy task, and there are lots of SO questions regarding this though none which solved my problem, obviously. So perhaps I am missing something obvious. I get a handle to my view by:
ImageView myView = (ImageView) getWindow().findViewById(R.id.MyViewID);
This works fine, but when calling getWidth(), getHeight(), getMeasuredWidth(), getLayoutParams().width, etc., they all return 0. I have also tried manually calling measure() on the view followed by a call to getMeasuredWidth(), but that has no effect.
I have tried calling these methods and inspecting the object in the debugger in my activity's onCreate() and in onPostCreate(). How can I figure out the exact dimensions of this view at runtime?
Use the ViewTreeObserver on the View to wait for the first layout. Only after the first layout will getWidth()/getHeight()/getMeasuredWidth()/getMeasuredHeight() work.
ViewTreeObserver viewTreeObserver = view.getViewTreeObserver();
if (viewTreeObserver.isAlive()) {
viewTreeObserver.addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
#Override
public void onGlobalLayout() {
view.getViewTreeObserver().removeOnGlobalLayoutListener(this);
viewWidth = view.getWidth();
viewHeight = view.getHeight();
}
});
}
There are actually multiple solutions, depending on the scenario:
The safe method, will work just before drawing the view, after the layout phase has finished:
public static void runJustBeforeBeingDrawn(final View view, final Runnable runnable) {
final OnPreDrawListener preDrawListener = new OnPreDrawListener() {
#Override
public boolean onPreDraw() {
view.getViewTreeObserver().removeOnPreDrawListener(this);
runnable.run();
return true;
}
};
view.getViewTreeObserver().addOnPreDrawListener(preDrawListener);
}
Sample usage:
ViewUtil.runJustBeforeBeingDrawn(yourView, new Runnable() {
#Override
public void run() {
//Here you can safely get the view size (use "getWidth" and "getHeight"), and do whatever you wish with it
}
});
On some cases, it's enough to measure the size of the view manually:
view.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
int width=view.getMeasuredWidth();
int height=view.getMeasuredHeight();
If you know the size of the container:
val widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(maxWidth, View.MeasureSpec.AT_MOST)
val heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(maxHeight, View.MeasureSpec.AT_MOST)
view.measure(widthMeasureSpec, heightMeasureSpec)
val width=view.measuredWidth
val height=view.measuredHeight
if you have a custom view that you've extended, you can get its size on the "onMeasure" method, but I think it works well only on some cases :
protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
final int newHeight= MeasureSpec.getSize(heightMeasureSpec);
final int newWidth= MeasureSpec.getSize(widthMeasureSpec);
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
If you write in Kotlin, you can use the next function, which behind the scenes works exactly like runJustBeforeBeingDrawn that I've written:
view.doOnPreDraw { actionToBeTriggered() }
Note that you need to add this to gradle (found via here) :
android {
kotlinOptions {
jvmTarget = "1.8"
}
}
implementation 'androidx.core:core-ktx:#.#'
Are you calling getWidth() before the view is actually laid out on the screen?
A common mistake made by new Android developers is to use the width
and height of a view inside its constructor. When a view’s
constructor is called, Android doesn’t know yet how big the view will
be, so the sizes are set to zero. The real sizes are calculated during
the layout stage, which occurs after construction but before anything
is drawn. You can use the onSizeChanged() method to be notified of
the values when they are known, or you can use the getWidth() and
getHeight() methods later, such as in the onDraw() method.
Based on #mbaird's advice, I found a workable solution by subclassing the ImageView class and overriding onLayout(). I then created an observer interface which my activity implemented and passed a reference to itself to the class, which allowed it to tell the activity when it was actually finished sizing.
I'm not 100% convinced that this is the best solution (hence my not marking this answer as correct just yet), but it does work and according to the documentation is the first time when one can find the actual size of a view.
Here is the code for getting the layout via overriding a view if API < 11 (API 11 includes the View.OnLayoutChangedListener feature):
public class CustomListView extends ListView
{
private OnLayoutChangedListener layoutChangedListener;
public CustomListView(Context context)
{
super(context);
}
#Override
protected void onLayout(boolean changed, int l, int t, int r, int b)
{
if (layoutChangedListener != null)
{
layoutChangedListener.onLayout(changed, l, t, r, b);
}
super.onLayout(changed, l, t, r, b);
}
public void setLayoutChangedListener(
OnLayoutChangedListener layoutChangedListener)
{
this.layoutChangedListener = layoutChangedListener;
}
}
public interface OnLayoutChangedListener
{
void onLayout(boolean changed, int l, int t, int r, int b);
}
You can check this question. You can use the View's post() method.
Use below code, it is give the size of view.
#Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
Log.e("WIDTH",""+view.getWidth());
Log.e("HEIGHT",""+view.getHeight());
}
This works for me in my onClickListener:
yourView.postDelayed(new Runnable() {
#Override
public void run() {
yourView.invalidate();
System.out.println("Height yourView: " + yourView.getHeight());
System.out.println("Width yourView: " + yourView.getWidth());
}
}, 1);
I was also lost around getMeasuredWidth() and getMeasuredHeight() getHeight() and getWidth() for a long time.......... later i found that getting the view's width and height in onSizeChanged() is the best way to do this........ you can dynamically get your CURRENT width and CURRENT height of your view by overriding the onSizeChanged() method.
might wanna take a look at this which has an elaborate code snippet.
New Blog Post: how to get width and height dimensions of a customView (extends View) in Android http://syedrakibalhasan.blogspot.com/2011/02/how-to-get-width-and-height-dimensions.html
In Kotlin file, change accordingly
Handler().postDelayed({
Your Code
}, 1)
You can get both Position and Dimension of the view on screen
val viewTreeObserver: ViewTreeObserver = videoView.viewTreeObserver;
if (viewTreeObserver.isAlive) {
viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
//Remove Listener
videoView.viewTreeObserver.removeOnGlobalLayoutListener(this);
//View Dimentions
viewWidth = videoView.width;
viewHeight = videoView.height;
//View Location
val point = IntArray(2)
videoView.post {
videoView.getLocationOnScreen(point) // or getLocationInWindow(point)
viewPositionX = point[0]
viewPositionY = point[1]
}
}
});
}
If you need to know the dimensions of a View right after it is drawn you can simply call post() on that given View and send there a Runnable that executes whatever you need.
It is a better solution than ViewTreeObserver and globalLayout since it gets called repeatedly not just once.
This Runnsble will execute only once and you will know the views size.
works perfekt for me:
protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
{
base.OnElementPropertyChanged(sender, e);
CTEditor ctEdit = Element as CTEditor;
if (ctEdit == null) return;
if (e.PropertyName == "Text")
{
double xHeight = Element.Height;
double aHaight = Control.Height;
double height;
Control.Measure(LayoutParams.MatchParent,LayoutParams.WrapContent);
height = Control.MeasuredHeight;
height = xHeight / aHaight * height;
if (Element.HeightRequest != height)
Element.HeightRequest = height;
}
}