I have a class to which I need to add one or more Views. In this example, a single ImageView.
I can add views without a problem and align them using LayoutParameters, but when I try to align or center them somewhere along the vertical axis, they either stick to the top or don't appear at all (they are likely just out of view).
In the constructor I call a method fillView(), which happens after all dimensions and such are set.
fillView()
public void fillView(){
img = new ImageView(context);
rl = new RelativeLayout(context);
img.setImageResource(R.drawable.device_access_not_secure);
rl.addView(img, setCenter());
this.addView(rl, matchParent());
}
matchParent()
public LayoutParams matchParent(){
lp = new RelativeLayout.LayoutParams(
RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT);
lp.setMargins(0, 0, 0, 0);
return lp;
}
setCenter()
public LayoutParams setCenter(){
lp = new RelativeLayout.LayoutParams(
RelativeLayout.LayoutParams.WRAP_CONTENT, RelativeLayout.LayoutParams.WRAP_CONTENT);
lp.addRule(RelativeLayout.CENTER_IN_PARENT, RelativeLayout.TRUE); //This puts the view horizontally at the center, but vertically at the top
return lp;
}
Similarly, adding rules such as ALIGN_RIGHT or BELOW will work fine, but ALIGN_BOTTOM or CENTER_VERTICALLY will not.
I tried using both this method and the setGravity() a LinearLayout offers, with the same results.
You're adding your ImageView before you've added the RelativeLayout
While I still don't know why my method worked horizontally, but not vertically, I did solve the problem. The posted methods worked, the problem was hidden in onMeasure().
I previously set the dimensions by simply passing them to setMeasuredDimension(). I fixed the issue by also passing them to the layoutParams(). I also changed the integers I used to MeasureSpecs while I was at it.
I changed this:
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){
super.onMeasure(this.getT_Width(), this.getT_Heigth());
this.setMeasuredDimension(desiredHSpec, desiredWSpec);
}
to this:
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){
final int desiredHSpec = MeasureSpec.makeMeasureSpec(this.getT_heigth(), MeasureSpec.EXACTLY);
final int desiredWSpec = MeasureSpec.makeMeasureSpec(this.getT_width(), MeasureSpec.EXACTLY);
this.getLayoutParams().height = this.getT_heigth();
this.getLayoutParams().width = this.getT_width();
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int width = MeasureSpec.getSize(desiredWSpec);
int height = MeasureSpec.getSize(desiredHSpec);
setMeasuredDimension(width, height);
}
getT_Width() and getT_Heigth() are methods I used to get some custom dimensions I set elsewhere.
I hope this helps somebody.
Related
I have made class called ProgressButton that extended RelativeLayout.Now in main xml i added this class:
<com.tazik.progressbutton.ProgressButton
android:id="#+id/pb_button"
android:layout_width="200dp"
android:layout_height="wrap_content"/>
As you can see i added android:layout_width="200dp", now in ProgressButton class i want to get this size to create a button with this size:
public class ProgressButton extends RelativeLayout {
private AppCompatButton button;
public ProgressButton(Context context) {
super(context);
initView();
}
private void initView() {
initButton();
}
private void initButton() {
button = new AppCompatButton(getContext());
LayoutParams button_params = new LayoutParams(????, ViewGroup.LayoutParams.WRAP_CONTENT);
button_params.addRule(RelativeLayout.CENTER_IN_PARENT,RelativeLayout.TRUE);
button.setLayoutParams(button_params);
button.setText("click");
addView(button);
}
I want to create button exactly to size of relativeLayout, so how can i get layout_width in my custom view to set button_params width?
now in ProgressButton class i want to get this size to create a button with this size
As #MikeM. suggested in a comment. It could be as easy as giving that child view a width of MATCH_PARENT. See below...
LayoutParams button_params = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
With that in place you don't need to worry about the actual size because MATCH_PARENT will stretch your child view to occupy the whole parent's width...obviosuly respecting margins and paddings.
However, if you do need to know the parent's width, you should query that in onMeasure. I strongly suggest you to stay away from onMeasure whenever possible because it is a bit complex and it might take a lot of your development time.
Either way, in onMeasure you can know what measurements the parent view wants to give to its child views, this is based on the space available to render inside the parent and the layout params specified...
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int childWidth = 0;
if(widthSpecMode == MeasureSpec.AT_MOST){
//The parent doesn't want the child to exceed "childWidth", it doesn't care if it smaller than that, just not bigger/wider
childWidth = MeasureSpec.getSize(widthMeasureSpec);
}
else if(widthSpecMode == MeasureSpec.EXACTLY){
//The parent wants the child to be exactly "childWidth"
childWidth = MeasureSpec.getSize(widthMeasureSpec);
}
else {
//The parent doesn't know yet what its children's width will be, probably
//because it's still taking measurements
}
//IMPORTANT!!! set your desired measurements (width and height) or call the base class's onMeasure method. Do one or the other, NOT BOTH
setMeasuredDimension(dimens, dimens);
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
Add a few Log.d calls inside onMeasure for a better understanding of what's happening. Be aware that this method will be called multiple times.
Again, this is an unnecessary overkill for your case scenario. Setting MATCH_PARENT to the button should produce the results you want
I wrote a class that extends ViewGroup and override the method as follow:
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
for(int index = 0; index < getChildCount(); index++){
int width = MeasureSpec.getSize(widthMeasureSpec);
int height = MeasureSpec.getSize(heightMeasureSpec);
View child = getChildAt(index);
child.measure(View.MeasureSpec.makeMeasureSpec(width,
View.MeasureSpec.UNSPECIFIED), View.MeasureSpec
.makeMeasureSpec(height, View.MeasureSpec.UNSPECIFIED));
}
}
When I add subView to this viewGroup as follow:
String word = words[random.nextInt(words.length)];
RelativeLayout layout = new RelativeLayout(DraggableGridViewSampleActivity.this);
TextView text = new TextView(DraggableGridViewSampleActivity.this);
text.setText(word);
ImageView view = new ImageView(DraggableGridViewSampleActivity.this);
view.setImageBitmap(getThumb(word));
layout.addView(view);
RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.FILL_PARENT, RelativeLayout.LayoutParams.WRAP_CONTENT);
params.addRule(RelativeLayout.ALIGN_PARENT_RIGHT);
layout.addView(text, params);
dgv.addView(layout);
The effect is like: The textView can't be displayed completely. It seems that the parent view doesn't provide enough space for the textview to display.
How to have the textView displayed completely?
The textView can't be displayed completely. It seems that the parent
view doesn't provide enough space for the textview to display.
What exactly are you trying to do with the onMeasure method? Right now you're letting the super class handle an initial measure of the children(super.onMeasure) and then you remeasure the children again, this time giving them a MeasureSpec.UNSPECIFIED, basically telling the children they could be as big as they want.
You should always respect the MeasureSpec when you're measuring the children(or the parent should allow scrolling). If the parent says that the children should be AT_MOST 500 px then don't potentially step over this value when measuring the children by giving them a MeasureSpec of UNSPECIFIED.
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 wanted to create a custom LinearLayout (and later a custom ImageButton) that could take percentage values for both dimensions of size based on its parent's size regardless of the parent type (Relative or Linear). I was following this post: How to size an Android view based on its parent's dimensions, and it was very helpful, but I have a problem that those answers don't address.
When I place my Custom LinearLayout inside another LinearLayout, everything works as expected. My Custom LinearLayout covers the expected space (80% of the parent's width in the example below).
However if I place it inside a RelativeLayout, my screen always shows empty, I am not sure why this happens.
Here is my class:
public class ButtonPanel extends LinearLayout {
public ButtonPanel(Context context, AttributeSet attrs) {
super(context, attrs);
}
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int parentWidth = MeasureSpec.getSize(widthMeasureSpec);
int parentHeight = MeasureSpec.getSize(heightMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int newWidth = (int) Math.ceil(parentWidth * 0.8);
this.setMeasuredDimension(newWidth, parentHeight);
this.setLayoutParams(new RelativeLayout.LayoutParams(newWidth,parentHeight));
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
}
And here is my testing layout for the activity.
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<com.android.tests.views.ButtonPanel
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:background="#drawable/inner_panel"
android:gravity="center_horizontal"
android:layout_alignParentLeft="true"
android:layout_alignParentTop="true">
</com.android.tests.views.ButtonPanel>
</RelativeLayout>
In my activity all I do is set the Content View to the above layout.
(Incidentally, does anybody now how I could get the type of the parent dynamically for setting the new LayoutParameters? Above you'll see the parent type (RelativeLayout) hard-coded into the Custom View onMeasure function)
Thanks in advance for any help!
Is this exposed to be a problem?
this.setLayoutParams(new RelativeLayout.LayoutParams(newWidth,parentHeight)); // <-- a RelativeLayout params?
In the onMeasure function you could use something like this to know what class is the parent of the view.
this.getParent().getClass().getName()
This should also work
a instanceof B
or
B.class.isAssignableFrom(a.getClass())
When using "instanceof", you need to know the class of "B" at compile time. When using "isAssignableFrom" it can be dynamic and change during runtime.
If you are not compfortable with string comparison, you could also use enums.
Turns out my two inquiries in this post were more related than expected.
I realized that by setting my view's LayoutParams to a completely new instance, I was overwriting the layout positioning information needed by the Relative Layout to position my view.
By 'zeroing out' that information, my view has the right dimensions, but the layout doesn't know where to place it, so it simply doesn't.
The following code for the new onMeasure shows how just directly modifying the height and width of the LayoutParams already attached to my view I avoid both overwriting the layout position information and having to create new LayoutParams based on the parent's type.
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int specWidth = MeasureSpec.getSize(widthMeasureSpec);
int specHeight = MeasureSpec.getSize(heightMeasureSpec);
int newWidth = (int) Math.ceil(parentWidth * 0.8);
int newHeight = (int) Math.ceil(parentHeight * 0.8);
this.setMeasuredDimension(newWidth, newHeight);
this.getLayoutParams().height = newHeight;
this.getLayoutParams().width = newWidth;
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
Now, I'll be honest and say that this code is still not bug-free. Bringing the activity to the foreground and background multiple times constantly reduces the size of this custom view. The 0.8 reduction factor gets applied over and over each time the activity is brought up (I suspect the setting of the LayoutParams has to do with it, it might actually be unnecessary, but I haven't has time to test).
BUT, this still answered the question concerning this post, namely, why was my view not appearing at all despite having the right dimensions.
Is it possible to add views to a layout during the onLayout event of one of its Childs?
i.e.
FrameLayout contains View, in View.onLayout() I want to add views to the parent FrameLayout.
This is because the views I need to draw on the FrameLayout needs the child View dimensions (width, height) to assign them to particular positions on the FrameLayout.
I already try to do so, but nothing is getting drawn. Do you know how can I accomplish the same effect? or if I'm doing something wrong. Don't know why I'm unable to draw the views, event if I call invalidate.
Thanks.
Yes, it's possible. I have solved similar problem (placing a checkpoint Button into FrameLayout over SeekBar) using the following code (overriden methods from SeekBar):
#Override
protected void onLayout(final boolean changed, final int left, final int top, final int right, final int bottom) {
super.onLayout(changed, left, top, right, bottom);
View child = new Button(getContext());
//child measuring
int childWidthSpec = ViewGroup.getChildMeasureSpec(mWidthMeasureSpec, 0, LayoutParams.WRAP_CONTENT); //mWidthMeasureSpec is defined in onMeasure() method below
int childHeightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);//we let child view to be as tall as it wants to be
child.measure(childWidthSpec, childHeightSpec);
//find were to place checkpoint Button in FrameLayout over SeekBar
int childLeft = (getWidth() * checkpointProgress) / getMax() - child.getMeasuredWidth();
LayoutParams param = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
param.gravity = Gravity.TOP;
param.setMargins(childLeft, 0, 0, 0);
//specifying 'param' doesn't work and is unnecessary for 1.6-2.1, but it does the work for 2.3
parent.addView(child, firstCheckpointViewIndex + i, param);
//this call does the work for 1.6-2.1, but does not and even is redundant for 2.3
child.layout(childLeft, 0, childLeft + child.getMeasuredWidth(), child.getMeasuredHeight());
}
#Override
protected synchronized void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//we save widthMeasureSpec in private field to use it for our child measurment in onLayout()
mWidthMeasureSpec = widthMeasureSpec;
}
There is also ViewGroup.addViewInLayout() method (it's protected, so you can use it only if you override onLayout method of your Layout) which javadoc says its purpose is exactly what we discuss here, but I haven't understood why is it better than addView(). You can find it's usage in ListView.