Search code examples
androidandroid-layoutlistviewandroid-listview

ListView: show fixed header on top of scroll bar (or drawing a view on top of another with dispatchDraw)


I'm trying to achieve this effect that can be seen above on StickyListHeaders's sample app:

Basically I need to show a single, static, fixed header view on top of a ListView but bellow its scrollbar. I don't need anything related to sections or alphabetical indexing or anything like that.

I'm unable to figure out how to do this based on the source code of StickyListHeaders. I tried subclassing ListView and overriding dispatchDraw() like this:

protected void dispatchDraw(Canvas canvas)
{
    View view = LayoutInflater.from(getContext()).inflate(R.layout.header, this, false);
    drawChild(canvas, view, getDrawingTime());
    super.dispatchDraw(canvas);
}

But it doesn't work, no header is drawn.


Solution

  • Answering my own question. This ListView subclass is able to do what I wanted. The first element of the list can become fixed calling showFixedHeader():

    import android.content.Context;
    import android.graphics.Canvas;
    import android.util.AttributeSet;
    import android.view.View;
    import android.widget.ListAdapter;
    import android.widget.ListView;
    
    public class FixedHeaderListView extends ListView
    {
        private View fixedHeader = null;
        private boolean fixedHeaderLayoutDone = false;
        private boolean showFixedHeader = true;
    
        @SuppressWarnings("unused")
        public FixedHeaderListView(Context context)
        {
            super(context);
        }
    
        @SuppressWarnings("unused")
        public FixedHeaderListView(Context context, AttributeSet attrs)
        {
            super(context, attrs);
        }
    
        @SuppressWarnings("unused")
        public FixedHeaderListView(Context context, AttributeSet attrs, int defStyle)
        {
            super(context, attrs, defStyle);
        }
    
        public void showFixedHeader(boolean show)
        {
            this.showFixedHeader = show;
            requestLayout(); // Will cause layoutChildren() and dispatchDraw() to be called
        }
    
        @Override
        protected void layoutChildren()
        {
            super.layoutChildren();
    
            if (!fixedHeaderLayoutDone)
            {
                ListAdapter adapter = getAdapter();
                if (adapter != null && adapter.getCount() > 0)
                {
                    // Layout the first item in the adapter's data set as the fixed header
                    fixedHeader = adapter.getView(0, null, this);
                    if (fixedHeader != null)
                    {
                        // Measure and layout
    
                        LayoutParams layoutParams = (LayoutParams)fixedHeader.getLayoutParams();
                        if (layoutParams == null)
                        {
                            layoutParams = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
                        }
    
                        int heightMode = MeasureSpec.getMode(layoutParams.height);
                        if (heightMode == MeasureSpec.UNSPECIFIED)
                        {
                            heightMode = MeasureSpec.EXACTLY;
                        }
    
                        int heightSize = MeasureSpec.getSize(layoutParams.height);
                        int maxHeight = getHeight() - getListPaddingTop() - getListPaddingBottom();
                        if (heightSize > maxHeight)
                        {
                            heightSize = maxHeight;
                        }
    
                        int widthSpec = MeasureSpec.makeMeasureSpec(getWidth() - getListPaddingLeft() - getListPaddingRight(), MeasureSpec.EXACTLY);
                        int heightSpec = MeasureSpec.makeMeasureSpec(heightSize, heightMode);
                        fixedHeader.measure(widthSpec, heightSpec);
                        fixedHeader.layout(0, 0, fixedHeader.getMeasuredWidth(), fixedHeader.getMeasuredHeight());
    
                        // Flag as layout done
                        fixedHeaderLayoutDone = true;
                    }
                }
            }
        }
    
    
        @Override @SuppressWarnings("NullableProblems")
        protected void dispatchDraw(Canvas canvas)
        {
            super.dispatchDraw(canvas);
    
            if (fixedHeader != null && showFixedHeader)
            {
                drawChild(canvas, fixedHeader, getDrawingTime());
            }
        }
    
    }
    

    It's not heavily tested, but it's a good starting point.