I am currently optimising a complex view containing many nested views. therefor i have created a custom layout extending the relativelayout class.
Based on data set from outside the class i further build the view with childs.
The child building is done within the custom layout. I got it working and the performance gain is enourmous. But before i can create and add the childs i need to know the width of the view.
There are several ways of getting the width of the view:
1 add global layout listener
public void init(){
getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
#Override
public void onGlobalLayout() {
//width is known -> create child views
}
});
}
This solution does not always fire an event. For example when the view is inside a fragment and restored from a backstack state. Also there seem to be a 100 to 500ms delay before this event is triggered.
in onMeasure
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
w = MeasureSpec.getSize(widthMeasureSpec);
h = MeasureSpec.getSize(heightMeasureSpec);
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
setMeasuredDimension(w, h);
post(new Runnable() {
#Override
public void run() {
//width is known -> create child views
MyCustomViewGroup.this.postInvalidate();
}
});
}
The difficulty here is that adding views inside onmeasure will result into a call to onmeasure again. And endless loop is the result. Logic needed to prevent this. Could't figure out how.
3 add views in onLayout
#Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
//width is known -> create child views
}
Various unexpected layout problems. Views not respecting layoutparams and showing weird behaviour. Not really sure how to solve it.
hacky timer implementation
private Handler ha = new Handler();
private Runnable r;
public void init() {
if (w > 0) {
//width is known ->create childs
return;
}
r = new Runnable() {
private long time = 0;
#Override
public void run() {
Log.d(TAG, "run");
if (w > 0) {
//width is known ->create childs
} else {
init(); //width is not known -> check later
}
}
};
ha.postDelayed(r, 5);
}
Ironically the last solution works best for me.
I know its a hell of a hack.
is there is anyone out there knowing alternatives? or can give me tips.
I am using following code to animate expandable layout:
class ExpandAnimation extends Animation {
private View _view;
private int _startHeight;
private int _finishHeight;
public ExpandAnimation( View view, int startHeight, int finishHeight ) {
_view = view;
_startHeight = startHeight;
_finishHeight = finishHeight;
setDuration(500);
System.out.println(_startHeight);
System.out.println(_finishHeight);
}
#Override
protected void applyTransformation( float interpolatedTime, Transformation t ) {
int newHeight = (int)((_finishHeight - _startHeight) * interpolatedTime + _startHeight);
_view.getLayoutParams().height = newHeight;
_view.requestLayout();
}
#Override
public void initialize( int width, int height, int parentWidth, int parentHeight ) {
super.initialize(width, height, parentWidth, parentHeight);
}
#Override
public boolean willChangeBounds( ) {
return true;
}
};
This animation is created every time I click a button. It is called properly (checked System.out.println and it prints correct values) however in emulator animation runs only like once of 15 times. To be exact hiding it works great but expanding works only once a few times (on emulator, cant get it working on phone).
What could be the problem?
Thanks in forward
EDIT: layout I am trying to animate is FrameLayout. It has TextView as child and finishHeight is measured by textView measure height. The values are correct. I have also tried calling textView.requestLayout() in apply transformation to redraw layout but it is not working. It still expands only sometimes. If you need any more code feel free to ask.
Calling
((View) toExpand.getParent()).invalidate();
just after startAnimation solved my problem. Must check it on other devices but I think it will work.
public class MainActivity extends Activity {
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
TableView tv = new TableView(this);
tv.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT,LayoutParams.MATCH_PARENT));
setContentView(tv);
}
#Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.activity_main, menu);
return true;
}
}
public class TableView extends ViewGroup {
private Paint oval;
private RectF rect;
public TableView(Context context) {
super(context);
oval= new Paint(Paint.ANTI_ALIAS_FLAG);
oval.setColor(Color.GREEN);
}
public void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawOval(rect , oval);
}
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int wspec = MeasureSpec.makeMeasureSpec(
getMeasuredWidth(), MeasureSpec.EXACTLY);
int hspec = MeasureSpec.makeMeasureSpec(
getMeasuredHeight(), MeasureSpec.EXACTLY);
for(int i=0; i<getChildCount(); i++){
View v = getChildAt(i);
v.measure(wspec, hspec);
}
}
#Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
float w=r-l;
float h=b-t;
rect=new RectF(w/8,h/8,7*w/8,7*h/8);
float theta = (float) (2 * Math.PI / getChildCount());
for(int i=0; i< getChildCount(); i++) {
View v = getChildAt(i);
w = (rect.right-rect.left)/2;
h = (rect.bottom-rect.top)/2;
float half = Math.max(w, h)/2;
float centerX = rect.centerX()+w*FloatMath.cos(theta);
float centerY = rect.centerY()+h*FloatMath.sin(theta);
v.layout((int)(centerX-half),(int)(centerY-half),(int)(centerX+half),(int)(centerY+half));
}
}
}
Well there are almost NONE good and deep tutorials and hardly any piece of data on how to do custom layouts right, so i tried to figure out how its done, what i am trying to implement is a Layout that paints a green oval at the center of the screen, and i want every child of this layout to be layed out around the oval.
you can think of that oval as a poker table that i want the children of this layout to seat around it.
What currently happens by this code is that i get a white app scren with no oval, so i debugged it and saw that onDraw never gets called...
3 questions:
why is onDraw not getting called?
the sdk warns me that i shouldnt allocate new objects within onLayout method, so where should i calculate the RectF so it is ready for the onDraw call to come?
does calling super.onDraw() would make all children paint themselves? or should i explicitly invoke their draw()?
If I got it all wrong and you guys can guide me in the right direction, or have any links to examples or tutorials related to this subject that would be helpful too!
By default, onDraw() isn't called for ViewGroup objects. Instead, you can override dispatchDraw().
Alternatively, you can enable ViewGroup drawing by calling setWillNotDraw(false) in your TableView constructor.
EDIT:
For #2:
- Initialize it in the constructor, then just call rect.set() in your onLayout() method.
For #3:
- Yes, as far as I'm aware the super call will handle it, you shouldn't have to handle that unless you need to customize the way the children are drawn.
If you want that the canvas is be re-draw call invalidate(), and the method onDraw() will be re-executed
I have a graph view (custom view) in Android and a main view. Depending on the user's preference I want to add 1-5 graph views onto my main view but am not quite sure how to. (I'm using purely Java and not xml). I was reading that I might have to use a Relative Layout or something in order to stack views.
Any advice or suggestions are welcome
in your activity you probably have something like this towards the begining of your onCreate() method:
setContentView(R.layout.main);
inside your main.xml file you probably have an element that is some kind of layout. I will assume LinearLayout for now, but it works similarly with all types. You'll need to get a reference to this layout and to do that it must have an id. So if that layout does not have something like this in it you need to add it:
android:id="#+id/myMainLayout"
Then back in your java sometime after you've called setContentView() you can find the reference to your layout with something like this:
LinearLayout myLayout = (LinearLayout)findViewById(R.id.myMainLayout);
Once you have a reference to your layout you can add your graph views to it with something like this:
myLayout.addView(graph1);
myLayout.addView(graph2);
//etc...
If you want to skip the xml layout all together you are allowed to make your layout in java. To do that it would like this:
LinearLayout myLayout = new LinearLayout(YourActivity.this);
myLayout.addView(graph1);
myLayout.addView(graph2);
setContentView(myLayout);
Note that you can only call setContentView() once so you'll need to pass some kind of Layout to it if you want to add more than 1 View.
Edit:
I have never specifically tried but I would think you could call addView() from the constructor in your custom view:
public CustomView() {
this.addView(anotherView);
}
do you have a custom view for your layout too?
Here is an example of a custom view as graph. One needs to have a LinearLayout somewhere in the layout which has ID set to #+id/ll and size of the graph:
public class RootActivity extends Activity {
/** Called when the activity is first created. */
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
int[] graphData = {3,5,2,7,4,8,1,5,9};
LinearLayout ll = (LinearLayout) findViewById(R.id.ll);
GraphView graphView = new GraphView(this);
ll.addView(graphView);
//call this method with every new set of data
graphView.drawGraph(graphData);
}
class GraphView extends View{
int[] graphData;
Paint graphPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
int screenH;
int screenW;
int colW;
int colH;
int columnCount;
public GraphView(Context context) {
super(context);
graphPaint.setColor(Color.MAGENTA);
graphPaint.setStyle(Style.FILL);
}
#Override
public void onSizeChanged (int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
screenW = w;
screenH = h;
}
public void drawGraph(int[] graphData){
this.graphData = graphData;
columnCount = graphData.length;
invalidate();
}
#Override
public void onDraw(Canvas canvas) {
super.onDraw(canvas);
colW = (screenW - 10) / columnCount;
int graphStep = 20;
int columnSpace = 5;
canvas.drawText("GRAPH", 10, 10, graphPaint);
for (int i= 0 ; i < columnCount; i++){
//draw columns from bottom up
canvas.drawRect(
new Rect(
i * colW + 5,
screenH - 5 - (graphData[i] * graphStep),
i * colW + 5 + colW - columnSpace,
screenH - 5
),
graphPaint);
}
}
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;
}
}