Making a middle element to get stuck in the header

2020-02-25 08:09发布

问题:

I want to make an element that is shown in the middle of ScrollView (or ListView) at the first and then gets stuck in the header of screen when it’s scrolled.

It’s a prototype implementation in CSS+JS: http://jsfiddle.net/minhee/aPcv4/embedded/result/.

At first glance I would make ScrollView to include ListView, but the official docs says:

You should never use a ScrollView with a ListView, because ListView takes care of its own vertical scrolling. Most importantly, doing this defeats all of the important optimizations in ListView for dealing with large lists, since it effectively forces the ListView to display its entire list of items to fill up the infinite container supplied by ScrollView.

So, what approaches can I try to achieve this UI?

Update: I tried StickyListHeaders, but: “it is currently not possible to have interactive elements in the header, Buttons, switches, etc. will only work when the header is not stuck.” Plus, I find it’s not very suitable for this situation. I don’t need multiple headers, but just one middle element to get stuck in the header.

回答1:

I have used(or rather, tried to use) the StickyListHeaders library in the past. After having some issues with it, I came up with the following. It is not much different from what other posters have suggested.

The main layout file activity_layout.xml consists of a ListView and a LinearLayout which is invisible by default. Using the OnScrollListener's onScroll() method, the LinearLayout's visibility is toggled. You don't need to inflate another layout or add views dynamically to your layout's parent. This is what the onScroll method looks like:

public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
    if (firstVisibleItem > 3) {        // 5th row will stick
        llHeader.setVisibility(View.VISIBLE);
    } else {
        llHeader.setVisibility(View.GONE);
    }
}

Simply, toggle the visibility to get the desired effect. You can take a look at the following code. Its a working example of what you can expect. The activity contains a ListView with a strictly barebone extension of BaseAdapter. The ListView is populated with numbered Buttons(one on each row, starting at 0, and going up to 19).

public class StickyHeader extends Activity {    

    LinearLayout llHeader;  
    ListView lv;
    SHAdapter shAdapter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_layout);

        lv = (ListView) findViewById(R.id.listView1);        
        llHeader = (LinearLayout) findViewById(R.id.llHeader);        
        shAdapter = new SHAdapter();        
        lv.setAdapter(shAdapter);

        lv.setOnScrollListener(new OnScrollListener() {

            @Override
            public void onScrollStateChanged(AbsListView view, int scrollState) {}

            @Override
            public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
                if (firstVisibleItem > 3) {
                    llHeader.setVisibility(View.VISIBLE);
                } else {
                    llHeader.setVisibility(View.GONE);
                }
            }
        });
    }   

    public class SHAdapter extends BaseAdapter {

        Button btCurrent;
        int[] arr = new int[] {0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19};

        @Override
        public int getCount() {
            return 20;
        }

        @Override
        public Object getItem(int arg0) {
            return arr[arg0];
        }

        @Override
        public long getItemId(int position) {
            return 0;
        }

        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
            convertView = getLayoutInflater().inflate(R.layout.list_item_layout, null);         
            btCurrent = (Button) convertView.findViewById(R.id.button1);            

            if ((Integer)getItem(position) == 4) {
                btCurrent.setText("Number " + getItem(position) + " is sticky");
            } else {
                btCurrent.setText("" + getItem(position));
            }

            return convertView;
        }
    }
}

activity_layout.xml

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent" >

    <ListView
        android:id="@+id/listView1"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" >
    </ListView>

    <!-- This LinearLayout's visibility is toggled -->

    <!-- Improvement suggested by user 'ar34z' 
         (see comment section below) --> 
    <include layout="@layout/list_item_layout" />

</RelativeLayout>

list_item_layout

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/llHeader"
    android:layout_width="match_parent"
    android:layout_height="50dp"
    android:background="@color/white"
    android:orientation="vertical" >

    <Button
        android:id="@+id/button1"
        android:layout_gravity="center"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

</LinearLayout>


回答2:

This is the simplest code to illustrate the main idea:

listView.setOnScrollListener(new OnScrollListener() {
    ViewGroup mainView = (ViewGroup) findViewById(R.id.main);
    View pinnedView = null;

    @Override
    public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
        if (firstVisibleItem < PINNED_ITEM) {
            mainView.removeView(pinnedView);
            pinnedView = null;
        } else if (pinnedView == null) {
            pinnedView = adapter.getView(PINNED_ITEM, null, view);
            pinnedView.setBackgroundColor(0xFF000000);
            mainView.addView(pinnedView);
        }
    }

    @Override
    public void onScrollStateChanged(AbsListView view, int scrollState) {}
});

I have a FrameLayout which contains nothing but my ListView:

<FrameLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:id="@+id/main"
>
    <ListView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
    />
</FrameLayout>

PINNED_ITEM is the position of your item (for example, PINNED_ITEM = 2). This layout acts as an overlay for the list. The ScrollListener tracks the current visible items and if it detects that an item should be pinned, it adds it to the layout and removes it otherwise.

The line pinnedView.setBackgroundColor(0xFF000000); is needed to set an opaque background for the item. The item will be transparent if you don't do this. You can tweak the background according to your needs (for example, you can use a background from your current theme attribute).



回答3:

In order to accomplish this, i'll put the listView in a relativeLayout and add a listener on the scroll. Then in it, when the firstVisibleItem change, i'll duplicate the itemView you need and display it over your listView.

Here is a short sample of what I think. The only things is that the duplicate view has to be opaque.

import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.view.ViewGroup.LayoutParams;
import android.widget.AbsListView;
import android.widget.AbsListView.OnScrollListener;
import android.widget.ArrayAdapter;
import android.widget.LinearLayout;
import android.widget.ListView;
import android.widget.RelativeLayout;

public class MainActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        /*Create listView inside a relativelayout*/
        final RelativeLayout rl = new RelativeLayout(this);
        final ListView lv = new ListView(this);
        rl.addView(lv);
        /* Set it as a content view*/
        setContentView(rl);
        /*populate it*/
        String[] items = { "Cupcake",
                "Donut",
                "Eclair",
                "Froyo",
                "Gingerbread",
                "Honeycomb",
                "Ice Cream Sandwich",
                "Jelly Bean"};
        int size = 10;
        String[] arrayItems = new String[items.length*size];
        for(int i = 0; i < arrayItems.length; i++)
            arrayItems[i] = items[i%items.length] + " " + i;     
        /* Need to use a non transparent view*/
        final ArrayAdapter<String> adapter = new ArrayAdapter<String>(this,
                    R.layout.simple_list_item_1_opaque, arrayItems);        
        lv.setAdapter(adapter);

        /* Choose the item to stick*/
        final int itemToStick = 3;

        /* Create things needed for duplication */
        final RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
        params.addRule(RelativeLayout.ALIGN_PARENT_TOP, 1);
        final RelativeLayout.LayoutParams selectorParams = new RelativeLayout.LayoutParams(LayoutParams.MATCH_PARENT, lv.getDividerHeight());
        lv.setOnScrollListener(new OnScrollListener() {

            @Override
            public void onScrollStateChanged(AbsListView view, int scrollState) {
                // TODO Auto-generated method stub

            }

            @Override
            public void onScroll(AbsListView view, int firstVisibleItem,
                    int visibleItemCount, int totalItemCount) {
                if(itemToStick <= firstVisibleItem && rl.getChildCount() == 1)
                {
                    /* Put view in a linearlayout in order to be able to add the sepline */
                    LinearLayout ll = new LinearLayout(view.getContext());
                    ll.setOrientation(LinearLayout.VERTICAL);
                    ll.addView(adapter.getView(itemToStick, null, null),params); 
                    /* Create Divider */
                    View selector = new LinearLayout(view.getContext());
                    selector.setBackground(lv.getDivider());
                    /* add views*/
                    ll.addView(selector,selectorParams);
                    rl.addView(ll,params);

                }
                /* Remove view when scrolling up to it */
                else if(itemToStick > firstVisibleItem)
                {
                    if(rl.getChildCount() > 1)
                        rl.removeViewAt(1);
                }
            }
        });
    }

}

And simple_list_item_1_opaque:

<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@android:id/text1"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:textAppearance="?android:attr/textAppearanceListItemSmall"
    android:gravity="center_vertical"
    android:paddingStart="?android:attr/listPreferredItemPaddingStart"
    android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
    android:minHeight="?android:attr/listPreferredItemHeightSmall"
    android:background="#FFFFFF"
/>