I need to create a View Placeholder: a View that doesn't draw nothing, but has the same dimension of a given View. I'm trying to achieve that by doing so:
public class ViewPlaceHolder extends View {
View target;
public ViewPlaceHolder(View target) {
super(target.getContext());
this.target = target;
}
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
target.measure(widthMeasureSpec, heightMeasureSpec);
super.onMeasure(
MeasureSpec.makeMeasureSpec(target.getMeasuredWidth(), MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(target.getMeasuredHeight(), MeasureSpec.EXACTLY));
}
}
By now it doesn't work as expected. Any suggestions?
Thanks a lot
PS: I need a placeholder to make same animations and effects on a scrollView
PPS: additional explanation:
the result of target.getMeasuredWidth() and target.getMeasuredHeight() is correct, but the ViewPlaceholder has a complete random height (the width seem to be ok)
Sorry, I made an error on the layout hierarchy, now it works fine!
If the target view is drawn , then you can read its dimensions by calling the getWidth() and getHeight() methods. So, if these methods does not return 0 , you can apply the setLayoutParams on the ViewPlaceHolder. The only thing that you have to keep in mind is the Layout that will lay your ViewPlaceHolder. Suppose it is a RelativeLayout you can do something like this inside the ViewPlaceHolder constructor body :
public ViewPlaceHolder(View target) {
super(target.getContext());
this.target = target;
int height = target.getHeight();
int width = target.getWidth();
this.setLayoutParams(new RelativeLayout.LayoutParams(width,height));
this.invalidate();
}
Related
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.
I'm using the HorizontalListView provided here, and I'm trying to show custom views with a fixed height and width, like this:
public class MyView extends View {
public MyView(Context context) {
super(context);
init();
}
private void init() {
setBackgroundColor((int) (Math.random() * Integer.MAX_VALUE));
}
#Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
int h = getHeight(); // shows 255, correct height
}
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int w = MeasureSpec.makeMeasureSpec(425, MeasureSpec.EXACTLY);
int h = MeasureSpec.makeMeasureSpec(255, MeasureSpec.EXACTLY);
super.onMeasure(w, h);
}
}
When showing this MyView in a normal LinearLayout, the height and width are perfect. However, when I show the view in the HorizontalListView, the width is perfect, but the height is not. See this screenshot:
The width is 425px, which is correct, the height is only 160px instead of 255.
In the source of the HorizontalListView there is this method:
private void addAndMeasureChild(final View child, int viewPos) {
LayoutParams params = child.getLayoutParams();
if(params == null) {
params = new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT);
}
addViewInLayout(child, viewPos, params, true);
child.measure(MeasureSpec.makeMeasureSpec(getWidth(), MeasureSpec.AT_MOST),
MeasureSpec.makeMeasureSpec(getHeight(), MeasureSpec.AT_MOST));
}
Is there something I should change in this method, or is there something else to make this work?
When you do int h = MeasureSpec.makeMeasureSpec(255, MeasureSpec.EXACTLY);, that 255 value is a PX value. If you said the height was 200 DP in the XML that value will be adjusted according to the device. Not sure if the screen shown is HDPI or XHDPI (my guess is the latter), it will be multiplied, so your 200 DP becomes 300 PX in an HDPI phone (x1.5). You can either use a PX value in the XML or you can define a dimension DP value that's 200DP in a values.xml.
The preference would be the latter. You can use that dimension value in your XML and retrieve that from the Resources when you create your View. That means that if at some point you want to make it bigger you can just change one value and you're done.
Edit
I see you found the real problem. In any case, you should still use the dimension approach, it will make your code cleaner and it will work in any phone (255 will only look right in a specific combination).
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;
}
}
I am working on a subclass of FrameLayout that is supposed to rotate all of its children by 90 degrees. I am doing this to overcome the landscape-only camera limitation present in android 2.1 and below, by having the activity be in landscape, but placing my camera overlay into this framelayout overlay to cause it to appear as if it was portrait (this is how Layar does it) To accomplish this, I'm adapting Jeff Sharkey's code to rotate views. My problem is that I can rotate the Framelayout, but I cannot resize it to match the new dimensions. So on my g1, instead of a 320x480 portrait view over a 480x320 camera view in landscape, I get a 320x320 box in the middle showing my portrait view with the sides chopped off.
Here is my code so far:
public class RotateLayout extends FrameLayout {
private Matrix mForward = new Matrix();
private Matrix mReverse = new Matrix();
private float[] mTemp = new float[2];
public RotateLayout(Context context) {
super(context);
}
public RotateLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
/* (non-Javadoc)
* #see android.widget.FrameLayout#onMeasure(int, int)
*/
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//This didn't work:
//super.onMeasure(heightMeasureSpec, widthMeasureSpec);
}
/* (non-Javadoc)
* #see android.widget.FrameLayout#onSizeChanged(int, int, int, int)
*/
#Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
}
#Override
protected void dispatchDraw(Canvas canvas) {
canvas.rotate(270, getWidth()/2, getHeight()/2);
//This code will stretch the canvas to accommodate the new screen size. This is not what I want.
//float scaleX=(float)getHeight()/getWidth();
//float scaleY=(float)getWidth()/getHeight();
//canvas.scale(scaleX, scaleY, getWidth()/2, getHeight()/2);
mForward = canvas.getMatrix();
mForward.invert(mReverse);
canvas.save();
canvas.setMatrix(mForward); //This is the matrix we need to use for proper positioning of touch events
super.dispatchDraw(canvas);
canvas.restore();
}
#Override
public boolean dispatchTouchEvent(MotionEvent event) {
final float[] temp = mTemp;
temp[0] = event.getX();
temp[1] = event.getY();
mReverse.mapPoints(temp);
event.setLocation(temp[0], temp[1]);
return super.dispatchTouchEvent(event);
}
}
I have tried overriding OnMeasure to switch the X and Y dimensions of the View, but have not been able to get that to work.
Any help you can provide is greatly appreciated.
I had the same problem and managed to solve it.
Instead of rotating each view or the layout by hand, I used a LayoutAnimationController.
First, place a file in /res/anim/ called rotation.xml
<?xml version="1.0" encoding="utf-8"?>
<rotate
xmlns:android="http://schemas.android.com/apk/res/android"
android:fromDegrees="0"
android:toDegrees="-90"
android:pivotX="50%"
android:pivotY="50%"
android:duration="0" android:fillAfter="true">
</rotate>
Then, in your Activity's onCreate, do
#Override
public void onCreate(Bundle icicle) {
super.onCreate(icicle);
setContentView(R.layout.myscreen);
Animation rotateAnim = AnimationUtils.loadAnimation(this, R.anim.rotation);
LayoutAnimationController animController = new LayoutAnimationController(rotateAnim, 0);
FrameLayout layout = (FrameLayout)findViewById(R.id.MyScreen_ContentLayout);
layout.setLayoutAnimation(animController);
}
If you want to rotate elements that lie above your camera preview view (SurfaceHolder), simply place a FrameLayout above the SurfaceHolder, place all your elements in that FrameLayout and call the Layout "MyScreen_ContentLayout". Done.
Hope that helped someone out, took me quite a while to get everything together.
Using API level 11 and later you can use the method setRotation(degreesFloat); to change the rotation of a view programmatically, or you can use the XML attribute android:rotation="" to change it in your XML. There are also methods/attributes for changing only the X or Y values of a view's rotation: Android Docs - View (setRotation).
So nowadays as long as you're using API level 11 or above, you should be able to apply the rotation to a wrapper layout node. However, you probably will also have to change the dimensions of the top-level layout to match the dimensions you desire after the rotation. I.e. if you have a portrait view w/ dimensions 800x1280, you'll have to change them to 1280x800 in order for it to line up after rotating to landscape.
Using this library you can rotate whole view hierarchy https://github.com/rongi/rotate-layout
Like this
This is what has worked for me in general.
private void init() {
setRotation(90f);
}
public YourViewOrViewGroup(final Context context) {
super(context);
init();
}
... (all the View/ViewGroup constructors) ...
#Override
protected void onLayout(final boolean changed, final int l, final int t, final int r, final int b) {
super.onLayout(changed, l, t, r, b);
final int width = getWidth();
final int height = getHeight();
final int offset = Math.abs(width - height) / 2;
setTranslationX(-offset);
setTranslationY(offset);
}
#Override
protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
super.onMeasure(heightMeasureSpec, widthMeasureSpec);
}
What you want to do is swap the width with height and then put the X & Y offsets so that the view becomes full screen after the rotation.
The above is a 'landscape'-rotated version. To achieve a landscape inverted just apply 270-deg rotation. You can either modify code within the snippet or apply the rotation outside in a more generic way, i.e
final YourViewOrViewGroup layout = inflater.inflate(...);
if (currentOrientation.isInverted()) {
layout.setRotation(layout.getRotation + 180f);
}
this way you are able to embed the rotated View/ViewGroup within the xml definition and inflate 'port' and 'land' versions while the screen orientation changes, but this seems out of this topic.
Edit: actually it is much better to defer the offset setting until at least one layout pass is over.
This is due the fact that in my case after first onMeasure() the view would be drawn (before the offsets were set). Eventually it could be experienced as glitching because the view/layout would not get drawn within the final bounds at first.
(updated the snippet)
I think you forgot one line in your onMeasure.
#Override
protected synchronized void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(heightMeasureSpec, widthMeasureSpec);
setMeasuredDimension(getMeasuredHeight(), getMeasuredWidth());
}
Taken from the code for a vertical seekbar here:
How can I get a working vertical SeekBar in Android?
You might need to fiddle with the getMeasuredHeight() to make it the correct size of your screen.
Try turning off children clipping of your view root: call setClipChildren(false) on parent of your RotateLayout and in onMeasure method of your RotateLayout put these lines:
super.onMeasure(heightMeasureSpec, widthMeasureSpec);
setMeasuredDimension(getMeasuredHeight(), getMeasuredWidth());
I'm having basically the same problem as you and I still haven't tested my solution - I'll do it tomorrow and tell if it is working correctly.