Imagine you had the following design for a card that you'd be using through an app. A title, button, a divider, and then space for dynamic content indicated by the blue box. We can add anything we'd need inside the blue region but the frame for holding the content would be consistent. For example:
Card with Placeholder Region
If I was going to put two TextViews inside it might look like this, with the (trimmed down) view layout below:
Card with Two TextViews
<com.google.android.material.card.MaterialCardView>
<LinearLayout android:orientation="vertical">
<LinearLayout android:orientation="horizontal">
<TextView android:text="My Title" />
<com.google.android.material.button.MaterialButton android:text="ACTION" />
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="#000" />
<LinearLayout android:id="+#id/contentGoesHere">
<TextView android:text="First element" />
<TextView android:text="Second element" />
</LinearLayout>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
Ideally I'd like a custom view so that developers can just do the following and get the consistent stying, or add to the view programatically:
<com.customview.CustomView>
<TextView android:text="First element" />
<TextView android:text="Second element" />
</com.customview.CustomView>
My problem is that just extending LinearLayout and making a custom view wont work - it'll create the layout but there's no way to indicate that the inner LinearLayout is what I want the views to be added to, so any subviews added in the XML are ignored.
Do I need to make a custom ViewGroup and manually inflate the custom holder for the LinearLayout (help! onMeasure and onLayout!?) or is there an easier way to make a custom LinearLayout view with this styled frame around it?
My problem is that just extending LinearLayout and making a custom
view wont work - it'll create the layout but there's no way to
indicate that the inner LinearLayout is what I want the views to be
added to, so any subviews added in the XML are ignored.
You can manually move the child views into nested LinearLayout.
For example:
public class CustomView extends LinearLayout {
public CustomView(Context context) {
super(context);
}
public CustomView(Context context, #Nullable AttributeSet attrs) {
super(context, attrs);
}
public CustomView(Context context, #Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
#Override
protected void onFinishInflate() {
super.onFinishInflate();
final ViewGroup container = inflate(getContext(), R.layout.card_layout, this)
.findViewById(R.id.inner_content_container);
while (getChildCount() > 1) {
final View child = getChildAt(0);
removeView(child);
container.addView(child, child.getLayoutParams());
}
}
}
card_layout.xml
<com.google.android.material.card.MaterialCardView>
<LinearLayout android:orientation="vertical">
<LinearLayout android:orientation="horizontal">
<TextView android:text="My Title" />
<com.google.android.material.button.MaterialButton android:text="ACTION" />
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="#000" />
<LinearLayout
android:id="#+id/inner_content_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<!-- content goes here -->
</LinearLayout>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
...
<com.customview.CustomView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView android:text="First element" />
<TextView android:text="Second element" />
</com.customview.CustomView>
Related
Hopefully I'm not being completely thick here, but I'm unable to align the baseline of a TextView to a Guideline in a ConstraintLayout. It appears the guideline does not have a baseline, which is pretty annoying. Does anyone know how I might achieve this? Here's a bit of layout xml that doesn't work (this is within a ConstraintLayout):
<TextView
android:id="#+id/textToAlignBaseline"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="20dp"
android:maxLines="1"
android:paddingRight="6dp"
android:textAppearance="#style/TextStyles.Body"
app:layout_constraintBaseline_toBaselineOf="#+id/guidelineBottomMargin"
app:layout_constraintLeft_toLeftOf="parent" />
<Button
android:id="#+id/buttonToAlignBottom"
android:layout_width="wrap_content"
android:layout_height="39dp"
android:layout_marginRight="20dp"
android:background="#drawable/selector_button_bg"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:text="#string/clickme"
android:textAppearance="#style/TextStyles.Body"
app:layout_constraintBottom_toTopOf="#+id/guidelineBottomMargin"
app:layout_constraintRight_toRightOf="parent" />
<android.support.constraint.Guideline
android:id="#+id/guidelineBottomMargin"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_end="20dp" />
I had this question when I tried to implement a two-line list according to Material Design, where the "Two-line item" baseline should be 32dp from the top:
It looks like a plain View or Guideline doesn't have a baseline because it returns -1 from getBaseline. So what I ended up with is using an invisible TextView like this:
<TextView
android:id="#+id/guideline"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginTop="32dp"
android:textSize="0sp"
android:visibility="invisible"
app:layout_constraintTop_toTopOf="parent" />
And then use app:layout_constraintBaseline_toBaselineOf="#id/guideline" for aligning the other view to it.
I think I might have come up with a solution, I tried extending Baseline and returning 0 from 'getBaseline' but that was never called, so I tried extending AppCompatButton instead and returning 'getMeasuredHeight' from 'getBaseline' instead (just like ImageView does when 'baselineAlignBottom' is used) and this seems to work correctly now. The TextView just needs changing to align its baseline to the button instead of the guideline. I need to clean it up with attributes similar to ImageView but this is what I've got for now:
public class ButtonBottomBaseline extends android.support.v7.widget.AppCompatButton {
public ButtonBottomBaseline(Context context) {
super(context);
}
public ButtonBottomBaseline(Context context, AttributeSet attrs) {
super(context, attrs);
}
public ButtonBottomBaseline(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
#Override
public int getBaseline() {
return getMeasuredHeight();
}
}
I've found a workaround to align the text following exactly the Material Design guideline as mentioned by #robinst .
Firstly add an ImageView, with 0 as layout_width, your desired baseline as layout_height, no src and baselineAlignBottom set to true:
<ImageView
android:id="#+id/typeTextBaseline"
android:layout_width="0dp"
android:layout_height="24dp"
android:baselineAlignBottom="true"
android:visibility="invisible"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
Then you can align your TextView's baseline to this ImageView's baseline:
<TextView
android:id="#+id/typeTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:textAppearance="#style/TextAppearance.MaterialComponents.Overline"
app:layout_constraintBaseline_toBaselineOf="#id/typeTextBaseline"
app:layout_constraintStart_toStartOf="parent" />
I found this workaround when searching all implementations of View's getBaseline method, and the ImageView's implementation is as follow:
public int getBaseline() {
if (mBaselineAlignBottom) {
return getMeasuredHeight();
} else {
return mBaseline;
}
}
So, once you set baselineAlignBottom to true, the ImageView's baseline becomes its bottom edge and it is magically served as a guideline now:)
I've done this before with no issue so i know my mistake is subtle.
picker_dialog_layout.xml:
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="left|center_horizontal"
android:id="#+id/pickerDialog_title"
android:text="HELLO WORLD!!"/>
<NumberPicker
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignTop="#+id/pickerDialog_title"
android:id="#+id/pickerDialog_selector"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignTop="#+id/pickerDialog_selector"
android:orientation="horizontal">
<Button
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="5"
android:id="#+id/pickerDialog_cancel"
android:gravity="center"
android:text="Cancel"/>
<Button
android:layout_height="match_parent"
android:layout_width="0dp"
android:layout_weight="5"
android:id="#+id/pickerDialog_set"
android:gravity="center"
android:text="Set"/>
</LinearLayout>
</merge>
During the Custom Class constructor, PickerDialog(Context context, AttributeSet attrs, int defStyle) i call:
LayoutInflater.from(context).inflate(R.layout.picker_dialog_layout, this);
And in the parent XML:
<com.company.simonaddicott.controlpanel_1.PickerDialog
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="#+id/pickerDialog" />
The view itself does show, and from using layout bound tools in developer tools i can identify that the ui elements are present, or at least the boundaries are present (see below)
What am i missing from this to make there UI elements appear like they should??
My mistake was that I extended the custom view by LinearLayout, but was using RelativeLayout positioning on the picker_dialog_layout.xml.
The elements inside the layout were not showing as they had no relative parent element to be positioned against
The Problem: How to uniquely identify elements which are inside android Compound View?
Background:
In my application I used Compound View. Because It has two TextFields, one image and one seek bar in each row. The layout is working fine.
So in the layout there are two separate seek bars. I need to get the value of them in my activity.
I can identify each row separately because they have id. But the items inside the row repeat in each row.
My problem is
how can I identify each seek bar uniquely?
row_layout.xml
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android" >
<RelativeLayout
android:id="#+id/colunm"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingTop="40sp" >
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginLeft="6dip"
android:layout_marginRight="6dip"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="marquee"
android:fadingEdge="horizontal"
android:singleLine="true"
android:textAppearance="?android:attr/textAppearanceMedium" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="1"
android:textAppearance="?android:attr/textAppearanceSmall" />
<SeekBar
android:layout_width="fill_parent"
android:layout_height="wrap_content" />
</RelativeLayout>
</merge>
screen_layout.xml
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
<com.example.test.view.Compound
android:id="#+id/vol_Row1"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
custom:column_summary="TEST SUMMARY I"
custom:column_title="Test TITLE 1"
custom:image_Icon="#drawable/ic_action1" />
<com.example.test.view.Compound
android:id="#+id/vol_Row2"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
custom:column_summary="TEST SUMMARY 2"
custom:column_title="Test TITLE 2"
custom:image_Icon="#drawable/ic_action2" />
</LinearLayout>
row_layout class
public Compound(Context context, AttributeSet attrs) {
super(context, attrs);
TypedArray a=context.obtainStyledAttributes(attrs,R.styleable.Options, 0, 0);
String titleText=a.getString(R.styleable.Options_title);
String summaryText=a.getString(R.styleable.Options_summary);
int icon=a.getInt(R.styleable.Options_image_Icon,0);
a.recycle();
LayoutInflater inflater=(LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
inflater.inflate(R.layout.row_layout,this,true);
imageView=getChildAt(0);
imageView.setImageResource(icon);
title=getChildAt(1)
title.setText(titleText);
summary=getChildAt(2)
summary.setText(summaryText);
}
}
First, add an ID to the seekbar in your XML layout file.
<SeekBar
android:id="#+id/seek"
android:layout_width="fill_parent"
android:layout_height="wrap_content" />
Second, in your code, use findViewById on the instance of the Compound view of which you want to obtain the reference to it's seek bar:
Coumpund c1 = (Compound)findViewById(R.id.vol_Row1);
Coumpund c2 = (Compound)findViewById(R.id.vol_Row2);
SeekBar sb1 = (SeekBar)c1.findViewById(seek); // Gives you the SeekBar of c1
SeekBar sb2 = (SeekBar)c2.findViewById(seek); // Gives you the SeekBar of c2
I did it in this way. In compound view I gave unique ids to each element. Then in test code I could access them uniquely like this,
private View inflaterView;
LayoutInflater i=(LayoutInflater) activity.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
inflaterView=i.inflate(R.layout.screen_layout, null);
seekBar=(SeekBar) inflaterView.findViewById(R.id.vol_Row1).findViewById(R.id.seekbar);
I have an XML layout similar to this (the XML and code is a lot more complex but I've simplified for brevity):
<merge xmlns:android="http://schemas.android.com/apk/res/android" >
<TableLayout
android:layout_width="match_parent"
android:layout_height="wrap_content" >
<TableRow
android:layout_width="match_parent"
android:layout_height="wrap_content" >
<ImageButton
android:id="#+id/button01"
android:src="#drawable/key_01" />
<ImageButton
android:id="#+id/button02"
android:src="#drawable/key_02" />
<ImageButton
android:id="#+id/button03"
android:src="#drawable/key_03" />
</TableRow>
</TableLayout>
</merge>
I have this layout as a custom widget.
public class ThreeButtons extends LinearLayout
{
public ThreeButtons(Context context, AttributeSet attrs)
{
super(context, attrs);
final LayoutInflater inflator = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
inflator.inflate(R.layout.three_buttons, this);
if (!isInEditMode())
setClickListeners();
}
...
private void setClickListeners()
{
...
}
}
The issue is that I have an extra level of depth that I don't think should be there.
ie: the actual ThreeButtons class is a LinearLayout that only has a single child, which is the inflated XML.
I would have thought that it would make more sense for the widget to be created directly from the XML rather than appending it as a child. Is the way I'm doing it the only way to have custom widgets? Or is there a cleaner way?
Thanks,
Brad
Rather than extending a LinearLayout, you can have your widget extend TableLayout.
Then, within your Merge tag, simply contain the list of rows that you want inflated into your widget.
This eliminates the additional LinearLayout.
Example:
<merge ... >
<TableRow ... />
<TableRow ... />
<TableRow ... />
<TableRow ... />
</merge>
I have a custom view like this
public class ButtonBar extends HorizontalScrollView
{
public View mButtonRows;
public ButtonBar(Context context, AttributeSet attrs)
{
super(context, attrs);
LayoutInflater inflater = (LayoutInflater)
context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
mButtonRows = inflater.inflate(R.layout.toolbar, null);
// button click handling code goes here
addView(mButtonRows);
}
}
which is included in my main xml like this
<com.example.ButtonBar
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_below="#+id/pagecontent" />
and inflates an xml file like this:
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/ButtonsRow"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<Button
android:id="#+id/button1"
android:layout_height="wrap_content"
android:layout_width="fill_parent"
android:layout_weight="0.3"
android:text="button1"
/>
<Button
android:id="#+id/button2"
android:layout_height="wrap_content"
android:layout_width="fill_parent"
android:layout_weight="0.3"
android:text="button2"
/>
<Button
android:id="#+id/button3"
android:layout_height="wrap_content"
android:layout_width="fill_parent"
android:layout_weight="0.3"
android:text="button3"
/>
</LinearLayout>
(It currently only has three buttons, but more are going to be needed in later versions, hence the HorizontalScrollView.)
Looking in hierarchyviewer, the custom view does seem to be screen wide, but the LinearLayout is only as wide as the buttons it contains (about 2/3 of the screen at the current button size), despite having the fill_parent width set; the buttons don't stretch. If I set the LinearLayout's background to #android:drawable/bottom_bar (which is a png the width of the screen), the buttons properly resize; I realise I could do the same thing by creating my own images to match, but I'd much rather do it without if possible.
What am I doing wrong?
ETA: if I change HorizontalScollView to ScrollView, it works fine. Do HSVs just not allow their children to "fill_parent"?
ETA2: Setting android:fillViewport="true" in the main xml fixed it!
Setting android:fillViewport="true" in the main xml fixed it!
If you change to this, for each of the buttons, does it work?
android:layout_width="0px"
android:layout_weight="1"