Custom ViewGroup with children inserted at specific spot
Asked Answered
S

1

6

I have several Activities in my Android app that have the same basic structure, and I'm trying to make my layouts DRY. The duplicated code looks like the below. It contains a scrollable area with a footer that has "Back" and "Dashboard" links. There's also a FrameLayout being used to apply a gradient on top of the scrollable area.

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:custom="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">
    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1">
        <ScrollView
            android:layout_width="match_parent"
            android:layout_height="689px">
            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:orientation="vertical">

                <!-- THE REAL PAGE CONTENT GOES HERE -->

            </LinearLayout>
        </ScrollView>
        <ImageView
            android:src="@drawable/GradientBar"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_gravity="bottom" />
    </FrameLayout>
    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="50px"
        android:background="?attr/primaryAccentColor">
        <Button
          android:layout_width="wrap_content"
          android:layout_height="26px"
          android:layout_gravity="center_vertical"
          local:MvxBind="Click GoBackCommand" />
        <Button
          android:layout_width="wrap_content"
          android:layout_height="26px"
          local:MvxBind="Click ShowDashboardHomeCommand" />
    </FrameLayout>
</LinearLayout>

To de-dupcliate my Activities, I think what I need to do is create a custom ViewGroup inherited from a LinearLayout. In that code, load the above content from an XML file. Where I am lost is how to get the child content in the Activity to load into the correct spot. E.g. let's say my Activity now contains:

<com.myapp.ScrollableVerticalLayoutWithDashboard
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <!-- THE REAL PAGE CONTENT GOES HERE -->
    <TextView android:text"blah blah blah" />

</com.myapp.ScrollableVerticalLayoutWithDashboard>

Now how do I cause the "blah blah blah" to appear in the correct place? I'm pretty sure if I did this, I would either end up with "blah blah blah" at the top or bottom of the page, not in the middle of the ScrollView as desired.

I'm using API 21 / v5.0+. Technically I'm doing all this with Xamarin, but hopefully that's irrelevant to the answer?

EDIT: An example of what the result would look like is this. The footer and gradient are part of the custom ViewGroup, but the rest would be content within the custom ViewGroup.

Mockup

Sabina answered 22/2, 2016 at 18:38 Comment(2)
Can you show on a draw the result you would like please. You just want to put a textView in the middle of the scrollView ?Righteous
@user3549047 I've added a mockup from one of our designers. There is actually content above what's shown, but that is already handled by an include element. The forms that make up the real content of these pages can be quite long; this is a small part of this overall form/activity.Sabina
G
8

I don't know Xamarin so this is an native android solution, but should be easy to translate.

I think what I need to do is create a custom ViewGroup inherited from a LinearLayout.

Yes, you could extend the LinearLayout class.

Where I am lost is how to get the child content in the Activity to load into the correct spot.

In your custom implementation you need to handle the children manually. In the constructor of that custom class inflate the layout manually:

private LinearLayout mDecor;

public ScrollableVerticalLayoutWithDashboard(Context context, AttributeSet attrs) {
      super(context, attrs);
      // inflate the layout directly, this will pass through our addView method
      LayoutInflater.from(context).inflate(R.layout.your_layout, this);
}

and then override the addView()(which a ViewGroup uses to append it's children) method to handle different types of views:

    private LinearLayout mDecor;

    public void addView(View child, int index, ViewGroup.LayoutParams params) {
        // R.id.decor will be an id set on the root LinearLayout in the layout so we can know
        // the type of view
        if (child.getId() != R.id.decor) {
            // this isn't the root of our inflated view so it must be the actual content, like
            // the bla bla TextView
            // R.id.content will be an id set on the LinearLayout inside the ScrollView where
            // the content will sit
            ((LinearLayout) mDecor.findViewById(R.id.content)).addView(child, params);
            return;
        }
        mDecor = (LinearLayout) child; // keep a reference to this static part of the view
        super.addView(child, index, params); // add the decor view, the actual content will
        // not be added here
    }

In Xamarin you're looking for the https://developer.xamarin.com/api/member/Android.Views.ViewGroup.AddView/p/Android.Views.View/System.Int32/Android.Views.ViewGroup+LayoutParams/ method to override. Keep in mind that this is a simple implementation.

EDIT: Rather than putting a LinearLayout inside a LinearLayout, you could just use the 'merge' tag. Here's the final layout you'd want:

<?xml version="1.0" encoding="utf-8" ?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:custom="http://schemas.android.com/apk/res-auto">
  <FrameLayout
      android:id="@+id/svfFrame1"
      android:layout_width="match_parent"
      android:layout_height="0dp"
      android:layout_weight="1">
    <ScrollView
        android:layout_width="match_parent"
        android:layout_height="689px">
      <LinearLayout
          android:id="@+id/svfContentLayout"
          android:layout_width="match_parent"
          android:layout_height="wrap_content"
          android:orientation="vertical"
          android:paddingBottom="23px" />
    </ScrollView>
    <ImageView
        android:src="@drawable/GradientBar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom" />
  </FrameLayout>
  <FrameLayout
      android:id="@+id/svfFrame2"
      xmlns:android="http://schemas.android.com/apk/res/android"
      xmlns:local="http://schemas.android.com/apk/res-auto"
      android:layout_width="match_parent"
      android:layout_height="50px"
      android:background="?attr/primaryAccentColor">
    <Button
      android:id="@+id/FooterBackButton"
      android:layout_width="wrap_content"
      android:layout_height="26px"
      android:layout_gravity="center_vertical"
      android:layout_marginLeft="24px" />
    <Button
      android:id="@+id/FooterDashboardButton"
      android:layout_width="wrap_content"
      android:layout_height="26px"
      android:layout_gravity="center_vertical|right"
      android:layout_marginRight="24px" />
  </FrameLayout>
</merge>

And here's the final working C# view for Xamarin based on that layout:

public class ScrollableVerticalLayoutWithDashboard: LinearLayout
{
    public ScrollableVerticalLayoutWithDashboard(Context context, IAttributeSet attrs) : base(context, attrs)
    {
        LayoutInflater.From(context).Inflate(Resource.Layout.ScrollableVerticalFooter, this);
        base.Orientation = Orientation.Vertical;
    }

    public override void AddView(View child, int index, ViewGroup.LayoutParams @params)
    {
        // Check to see if the child is either of the two direct children from the layout
        if (child.Id == Resource.Id.svfFrame1 || child.Id == Resource.Id.svfFrame2)
        {
            // This is one of our true direct children from our own layout.  Add it "normally" using the base class.
            base.AddView(child, index, @params);
        }
        else
        {
            // This is content coming from the parent layout, not our own inflated layout.  It 
            //   must be the actual content, like the bla bla TextView.  Add it at the appropriate location.
            ((LinearLayout)this.FindViewById(Resource.Id.svfContentLayout)).AddView(child, @params);
        }
    }
}
Gunilla answered 22/2, 2016 at 20:32 Comment(2)
I got a working solution based on using a Merge element at the root of the layout. With your permission I'd like to edit your answer and put in the final code and layout that worked - would that be ok? You deserve credit for doing most of the work, but the answer above doesn't seem to work for me unless I have redundant children, which is advised against: developer.android.com/training/improving-layouts/…Sabina
@pbarranis Sure, feel free to edit(you could even include the Xamarin code which your question is more about), what I posted above was more of an idea than a full implementation. I based my answer on the layout file you posted which included the redundant LinearLayout(a merge tag is strongly recommended in your case to avoid a deeper view hierarchy).Gunilla

© 2022 - 2024 — McMap. All rights reserved.