2

We have a custom GridView which has headerView and footerView properties. I'm wondering if in Android, it's possible to set those properties from within a layout file.

XAML in Windows lets you do this easily since you can specify properties either via attributes (for things like strings, numbers or other simple types), or via nested elements (for any object type) with a ControlType:PropertyName syntax.

Here's a pseudo-version of what this would look like if Android supported something similar:

<MyCustomGrid
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <!-- This would set the 'headerView' property
         on 'MyCustomGrid' to a TextView -->
    <MyCustomGrid:headerView>

        <TextView android:text="I'm the Header TextView" />

    </MyCustomGrid:headerView>

</MyCustomGrid>

Obviously the above is not valid. But is it possible to do something similar in Android, or do I have to do it in the code-behind in the Activity/Fragment?

Mark A. Donohoe
  • 28,442
  • 25
  • 137
  • 286
  • You're familiar with custom attributes, so that would be an option. Define `headerView` and `footerView` attributes as `reference`s, and set their values to the IDs of the respective `View`s on the `` element, similar to how some `RelativeLayout` `layout_*` attributes refer to other `View`s. Or you could do something like your last question, and create custom `LayoutParams` for `MyCustomGrid`, and put a custom attribute there to designate a given `View` as a header or footer. – Mike M. Oct 14 '17 at 03:58
  • Interesting! I hadn't considered using the reference type. Will the LayoutManager allow you to specify nested items inside the MyCustomGrid element? If not, where would they go in the layout. (For the record, MyCustomGrid is a RecyclerView subclass that allows you to explicitly set header and footer views.) – Mark A. Donohoe Oct 14 '17 at 04:06
  • And again, put it in an answer so I can mark it! :) – Mark A. Donohoe Oct 14 '17 at 04:06
  • Well, I've not written a custom `LayoutManager` yet, but there's surely some way to get at those `View`s. I'm pretty sure you're going to have to intercept their adds to the `RecyclerView` anyway - by overriding its `addView()` - so your `MyCustomGrid` class could expose them through public methods, and the `LayoutManager` can grab them from those. Lemme think on it for a bit, make sure I'm going in the right direction. – Mike M. Oct 14 '17 at 04:20
  • Yeah, you are going to have to intercept the adds for those `View`s in the layout, because those will be added during inflation, before a `LayoutManager` is set, and `RecyclerView` tries to do a `LayoutParams` check with the `LayoutManager` on every added `View`, so that'll throw an NPE. You could do something like is shown in [this answer](https://stackoverflow.com/a/36947869), and in `addView()`, only call through to the `super` if the child's ID is not one of those you retrieved from the attributes in the constructor. Still trying to figure how to get them in the `LayoutManager`, though. – Mike M. Oct 14 '17 at 04:45
  • Oh, duh. We can override `setLayoutManager()` in `MyCustomGrid`, and if the passed `LayoutManager` is your custom class, just use public methods on it to set the header and footer `View`s we already grabbed in `addView()` during inflation. I'm gonna test this out, in case I'm missing something. Lemme know if that sounds like it'll work for ya. – Mike M. Oct 14 '17 at 04:55
  • It's funny! That's the exact same approach I'm doing with one difference. I've overridden the setLayoutManager to throw an exception, and I'm setting my own layout manager in the constructor of my RecyclerView subclass. Haven't tried the 'addView' part yet. Let me know if your attempt works. – Mark A. Donohoe Oct 14 '17 at 05:56
  • Hmm, yeah, the `addView()` thing works, inasmuch as it successfully holds off adding the designated header and footer `View`s. However, we eventually have to add those `View`s to the `RecyclerView`, or they won't be laid out, and when we do that, `RecyclerView` expects them to have `ViewHolder`s attached to their `LayoutParams`, which they won't have, since they didn't come from an `Adapter`, and we get another NPE. I'm starting to think that this approach isn't really viable. You might have to implement this like `ListView` does, and create a wrapper `Adapter` that adds the header and footer. – Mike M. Oct 14 '17 at 06:24
  • Actually, when you intercept the add, you can create a new CustomLayoutParams based on the view's existing LayoutParams (there's a constructor overload) and assign it back to the view before calling super. That would address that issue. But there's still the other issue of identifying which view(s) that are added that way are for the headers and/or footers. I'm starting to think it's just not a good design usability-wise, even if it is technically possible and fun to figure out. :) – Mark A. Donohoe Oct 14 '17 at 19:36
  • I'm not sure if I'm following you correctly, but we have the IDs and references for those `View`s, so it shouldn't be a problem tracking them in the `LayoutManager`. There are several other issues, though. The `ViewHolder` in `LayoutParams` is package private, and we'd need reflection to get at it. Also, the `ViewHolder` is assigned an item view type, also package private, and depending on how the `Recycler` works, it may very well throw a wrench in that if it's unassigned, or we try to fake, and we'd likely need access to the `Adapter` to try that, which would make this even more convoluted. – Mike M. Oct 15 '17 at 04:15
  • I think we're not explaining this clearly to the other. I was referring to getting the proper layout params on the added views, which is easy. You are referring to the ViewHolders. But even there, that too is easy because from within the adapter you can just new up a new ViewHolder directly (no subclass needed) to hold the view. The ViewHolder doesn't need to know about what the layout contains since we had already handled that when the controls were added. – Mark A. Donohoe Oct 15 '17 at 04:33
  • All of the above works fine, but my point about it still not being a good idea is say you have three items added in between the tags of the RecyclerView subclass. Yes, we can grab those instances from the outside, and even hold plain 'View' instances internally. But what says 'View 1 is a header and Views 2 and 3 are footers' to the *LayoutManager*? You'd have to define new attributes to apply to those views.... – Mark A. Donohoe Oct 15 '17 at 04:35
  • But if you go that approach, why not define attributes right on the RecyclerView itself, which is exactly what I ended up doing... I added a headerView and footerView attribute (type reference) which I then instantiate in the constructor of the recyclerView, and since they're from a specific attribute, I internally know which is the header and which is the footer, without caring what the actual header and footer is. Then in the layout, as stated above, I just throw them in a generic ViewHolder (no need to call the adapter to get it, although the adapter does allocate a position for it.) – Mark A. Donohoe Oct 15 '17 at 04:37
  • This thread is getting long. Lemme know if you want me to put something together and send it over so you can see what I mean. – Mark A. Donohoe Oct 15 '17 at 04:37
  • Yeah, we're probably not. From your comments below, I took it that you're trying to do this completely in the custom `LayoutManager` and `RecyclerView`. If you're going to have to involve the `Adapter` anyway, why don't you just do this with a wrapper `Adapter`? You wouldn't need a custom `LayoutManager` at all. Also, I didn't realize what you were asking about discerning the header/footer. I thought that's what you were already doing, as that's how I meant to describe it in my comments, though maybe it was a biased assumption, 'cause that's just how I've been doing it on this side all along. – Mike M. Oct 15 '17 at 05:00
  • Anyhoo, if you want to put your code up somewhere, I'd be interested to see what you've got. – Mike M. Oct 15 '17 at 05:00
  • Did you happen to get a chance to put together your example? I had some time today, so I played around with this a bit, out of curiosity, and I never got past the `ViewHolder` issue, with respect to the `LayoutParams` field, that is. AFAICT, strictly `RecyclerView` has to be what sets that (apart from reflection), because package private, and that only happens during an `Adapter` call, or a recycle event. I keep crashing as soon as `RecyclerView` tries to layout. I'm also not sure how a `ViewHolder` subclass is unnecessary, as that class is abstract. I'm really curious to see how you solved it – Mike M. Oct 16 '17 at 03:27
  • I actually have an implementation working that I'm kind of happy with. In short, I'm using both a custom Adapter as well as a custom LayoutManager. The adapter is where I have properties for headerView and footerView. You can also specify them via layout resource IDs via attributes. I override getItemCount() taking into consideration if I have a header and/or a footer adding 0, 1 or 2 to the actual item count as needed. continued... – Mark A. Donohoe Oct 16 '17 at 03:36
  • Then in my custom LayoutManager (which knows about that specific Adapter type) I use a ViewType of -1 for the header and -2 for the footer and handle just those two cases internally, both creating the ViewHolder and setting the correct LayoutParams. Everything else is handled by the subclass of my adapter just as any other adapter would. In short, to the RecyclerView, the header and footer are just part of the data except I create the ViewModels/LayoutParams for them in the adapter. Make sense? – Mark A. Donohoe Oct 16 '17 at 04:40

1 Answers1

1

Yes, you can.

You can create a Custom View by extending the GridView class and add some logic through attributes. This will not work as an attached property from XAML (Like Grid.Column or Grid.Row from XAML UWP) but you can do something like this:

<com.teste.widget.MarqueIVGridView
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    app:my_header="@layout/my_header"
/>

Don't forget to add the namespace at the root of your layout:

xmlns:app="http://schemas.android.com/apk/res-auto"

Google has this sample: HeaderGridView

It uses a different approach, if you copy this class you will just need to use this "HeaderGridView" and call addHeaderView method from it sending your header view inflated.

Feel free to ask any question and It will be a pleasure to answer.

Thiago Souto
  • 761
  • 5
  • 13
  • This approach has the header content in a separate layout file. Could it work if the header view is defined with an ID in between the grid's opening and closing tags so everything is right there in the layout? I have to think so since RecyclerView is a subclass of ViewGroup. I'd just manually find and detach the views (holding references to them), then use them as part of the layout process. Perhaps I can even use custom attributes to mark them as header cells. – Mark A. Donohoe Oct 14 '17 at 04:15
  • The approach from XML would work as a header if you want. You will need to create a custom View inflating a custom layout, like a LinearLayout with a vertical orientation and GridView below your header(could be a TextView or other components). – Thiago Souto Oct 14 '17 at 04:22
  • If you try extending the component (Like GridView or RecyclerView) you will need to accept an external View as a Header. RecyclerView uses a ViewHolder Pattern (the adapter will create a ViewHolder for the header using an external layout XML) – Thiago Souto Oct 14 '17 at 04:25
  • yes, the *adapter* will create that, but the layout manager can layout anything it wants, including items not from the adapter. That's what I'm trying to do... lay out items that aren't part of the adapter's items set. But I still want the header and footer in the scrollable area. I just don't want to have to shove them into the adapter logic because then you have to start dealing with offsets and if/elses everywhere. I'm just inserting one or two extra, static, non-recycled views into the layout pass. – Mark A. Donohoe Oct 14 '17 at 04:27
  • Lemme play around with my design some more to see if I can get it working. I'll also take a look at your approach, using separate layout files because this too is a good way, and is probably more 'Androidy' than the way I'm doing it. – Mark A. Donohoe Oct 14 '17 at 04:28
  • You can remove the vertical scroll from the Gridview and delegate it to the LinearLayout or use a ScrollView instead. With this approach, you can place a "header" above the GridView/RecyclerView and a "footer" below the RecyclerView. Good luck and have fun :) – Thiago Souto Oct 14 '17 at 04:37
  • But won't that interfere with the recycling of the views? There could be hundreds of items between the header and footer. – Mark A. Donohoe Oct 14 '17 at 04:38
  • I see ... This approach will give you a big headache, sorry. I can not think of a better approach than using the adapter for the RecyclerView or the CustomGridView. If you find something, please edit your question. It would be amazing learning another way to solve this problem. Good luck. – Thiago Souto Oct 14 '17 at 04:47
  • Much appreciated! :) – Mark A. Donohoe Oct 14 '17 at 04:56