From 6c454085dd313102128f79740da535fe404fec1b Mon Sep 17 00:00:00 2001 From: Julian Winkler Date: Thu, 29 Aug 2024 13:56:58 +0200 Subject: [PATCH] ListView: implement header and footer Views Underlying HeaderViewListAdapter class is copied from AOSP --- src/api-impl/android/widget/AbsListView.java | 4 +- .../android/widget/HeaderViewListAdapter.java | 228 +++++++++++++++++- src/api-impl/android/widget/ListView.java | 67 ++++- 3 files changed, 289 insertions(+), 10 deletions(-) diff --git a/src/api-impl/android/widget/AbsListView.java b/src/api-impl/android/widget/AbsListView.java index 868eb044..3159ec76 100644 --- a/src/api-impl/android/widget/AbsListView.java +++ b/src/api-impl/android/widget/AbsListView.java @@ -8,7 +8,7 @@ import android.view.ViewGroup; public abstract class AbsListView extends AdapterView { public boolean mIsChildViewEnabled = false; // this field gets directly accessed by androidx DropDownListView - private Observer observer = new Observer(); + protected Observer observer = new Observer(); public AbsListView(Context context) { super(context); @@ -79,7 +79,7 @@ public abstract class AbsListView extends AdapterView { public interface SelectionBoundsAdjuster {} - private class Observer extends DataSetObserver { + class Observer extends DataSetObserver { @Override public void onChanged() { diff --git a/src/api-impl/android/widget/HeaderViewListAdapter.java b/src/api-impl/android/widget/HeaderViewListAdapter.java index 4350e7d8..d5b7afb5 100644 --- a/src/api-impl/android/widget/HeaderViewListAdapter.java +++ b/src/api-impl/android/widget/HeaderViewListAdapter.java @@ -1,5 +1,231 @@ +/* + * Copyright (C) 2006 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package android.widget; -public class HeaderViewListAdapter { +import java.util.ArrayList; + +import android.database.DataSetObserver; +import android.view.View; +import android.view.ViewGroup; + +/** + * ListAdapter used when a ListView has header views. This ListAdapter + * wraps another one and also keeps track of the header views and their + * associated data objects. + *

This is intended as a base class; you will probably not need to + * use this class directly in your own code. + */ +public class HeaderViewListAdapter implements ListAdapter, Filterable { + + private final ListAdapter mAdapter; + + // These two ArrayList are assumed to NOT be null. + // They are indeed created when declared in ListView and then shared. + ArrayList mHeaderViewInfos; + ArrayList mFooterViewInfos; + + // Used as a placeholder in case the provided info views are indeed null. + // Currently only used by some CTS tests, which may be removed. + static final ArrayList EMPTY_INFO_LIST = + new ArrayList(); + + boolean mAreAllFixedViewsSelectable; + + public HeaderViewListAdapter(ArrayList headerViewInfos, + ArrayList footerViewInfos, + ListAdapter adapter) { + mAdapter = adapter; + + if (headerViewInfos == null) { + mHeaderViewInfos = EMPTY_INFO_LIST; + } else { + mHeaderViewInfos = headerViewInfos; + } + + if (footerViewInfos == null) { + mFooterViewInfos = EMPTY_INFO_LIST; + } else { + mFooterViewInfos = footerViewInfos; + } + + mAreAllFixedViewsSelectable = + areAllListInfosSelectable(mHeaderViewInfos) + && areAllListInfosSelectable(mFooterViewInfos); + } + + public int getHeadersCount() { + return mHeaderViewInfos.size(); + } + + public int getFootersCount() { + return mFooterViewInfos.size(); + } + + public boolean isEmpty() { + return mAdapter == null || mAdapter.isEmpty(); + } + + private boolean areAllListInfosSelectable(ArrayList infos) { + if (infos != null) { + for (ListView.FixedViewInfo info : infos) { + if (!info.isSelectable) { + return false; + } + } + } + return true; + } + + public boolean removeHeader(View v) { + for (int i = 0; i < mHeaderViewInfos.size(); i++) { + ListView.FixedViewInfo info = mHeaderViewInfos.get(i); + if (info.view == v) { + mHeaderViewInfos.remove(i); + + mAreAllFixedViewsSelectable = + areAllListInfosSelectable(mHeaderViewInfos) + && areAllListInfosSelectable(mFooterViewInfos); + + return true; + } + } + + return false; + } + + public boolean removeFooter(View v) { + for (int i = 0; i < mFooterViewInfos.size(); i++) { + ListView.FixedViewInfo info = mFooterViewInfos.get(i); + if (info.view == v) { + mFooterViewInfos.remove(i); + + mAreAllFixedViewsSelectable = + areAllListInfosSelectable(mHeaderViewInfos) + && areAllListInfosSelectable(mFooterViewInfos); + + return true; + } + } + + return false; + } + + public int getCount() { + if (mAdapter != null) { + return getFootersCount() + getHeadersCount() + mAdapter.getCount(); + } else { + return getFootersCount() + getHeadersCount(); + } + } + + public Object getItem(int position) { + // Header (negative positions will throw an IndexOutOfBoundsException) + int numHeaders = getHeadersCount(); + if (position < numHeaders) { + return mHeaderViewInfos.get(position).data; + } + + // Adapter + final int adjPosition = position - numHeaders; + int adapterCount = 0; + if (mAdapter != null) { + adapterCount = mAdapter.getCount(); + if (adjPosition < adapterCount) { + return mAdapter.getItem(adjPosition); + } + } + + // Footer (off-limits positions will throw an IndexOutOfBoundsException) + return mFooterViewInfos.get(adjPosition - adapterCount).data; + } + + public long getItemId(int position) { + int numHeaders = getHeadersCount(); + if (mAdapter != null && position >= numHeaders) { + int adjPosition = position - numHeaders; + int adapterCount = mAdapter.getCount(); + if (adjPosition < adapterCount) { + return mAdapter.getItemId(adjPosition); + } + } + return -1; + } + + public boolean hasStableIds() { + if (mAdapter != null) { + return mAdapter.hasStableIds(); + } + return false; + } + + public View getView(int position, View convertView, ViewGroup parent) { + // Header (negative positions will throw an IndexOutOfBoundsException) + int numHeaders = getHeadersCount(); + if (position < numHeaders) { + return mHeaderViewInfos.get(position).view; + } + + // Adapter + final int adjPosition = position - numHeaders; + int adapterCount = 0; + if (mAdapter != null) { + adapterCount = mAdapter.getCount(); + if (adjPosition < adapterCount) { + return mAdapter.getView(adjPosition, convertView, parent); + } + } + + // Footer (off-limits positions will throw an IndexOutOfBoundsException) + return mFooterViewInfos.get(adjPosition - adapterCount).view; + } + + public int getItemViewType(int position) { + int numHeaders = getHeadersCount(); + if (mAdapter != null && position >= numHeaders) { + int adjPosition = position - numHeaders; + int adapterCount = mAdapter.getCount(); + if (adjPosition < adapterCount) { + return mAdapter.getItemViewType(adjPosition); + } + } + + return /*ITEM_VIEW_TYPE_HEADER_OR_FOOTER*/ -2; + } + + public int getViewTypeCount() { + if (mAdapter != null) { + return mAdapter.getViewTypeCount(); + } + return 1; + } + + public void registerDataSetObserver(DataSetObserver observer) { + if (mAdapter != null) { + mAdapter.registerDataSetObserver(observer); + } + } + + public void unregisterDataSetObserver(DataSetObserver observer) { + if (mAdapter != null) { + mAdapter.unregisterDataSetObserver(observer); + } + } + public ListAdapter getWrappedAdapter() { + return mAdapter; + } } diff --git a/src/api-impl/android/widget/ListView.java b/src/api-impl/android/widget/ListView.java index fa734639..6975e6b2 100644 --- a/src/api-impl/android/widget/ListView.java +++ b/src/api-impl/android/widget/ListView.java @@ -1,5 +1,6 @@ package android.widget; +import java.util.ArrayList; import android.content.Context; import android.graphics.drawable.Drawable; import android.util.AttributeSet; @@ -7,6 +8,21 @@ import android.view.View; public class ListView extends AbsListView { + static class FixedViewInfo { + public View view; + public Object data; + public boolean isSelectable; + + public FixedViewInfo(View view, Object data, boolean isSelectable) { + this.view = view; + this.data = data; + this.isSelectable = isSelectable; + } + } + + private ArrayList headerViews = new ArrayList(); + private ArrayList footerViews = new ArrayList(); + public ListView(Context context) { super(context); } @@ -15,35 +31,72 @@ public class ListView extends AbsListView { super(context, attributeSet); } + public ListView(Context context, AttributeSet attributeSet, int defStyleAttr) { + super(context, attributeSet, defStyleAttr); + } + + @Override + public void setAdapter(ListAdapter adapter) { + if (getHeaderViewsCount() > 0 || getFooterViewsCount() > 0) { + adapter = new HeaderViewListAdapter(headerViews, footerViews, adapter); + } + super.setAdapter(adapter); + } + public int getDividerHeight() {return 0;} public Drawable getDivider() {return null;} public void setTextFilterEnabled(boolean enabled) {} - public void addHeaderView(View v, Object data, boolean isSelectable) {} + public void addHeaderView(View v, Object data, boolean isSelectable) { + headerViews.add(new FixedViewInfo(v, data, isSelectable)); + if (getAdapter() instanceof HeaderViewListAdapter) { + observer.onChanged(); + } else if (getAdapter() != null) { + setAdapter(getAdapter()); + } + } public void setDrawSelectorOnTop(boolean dummy) {} - public void addHeaderView(View view) {} + public void addHeaderView(View view) { + addHeaderView(view, null, true); + } - public boolean removeHeaderView(View view) { return true; } + public boolean removeHeaderView(View view) { + boolean result = false; + if (getAdapter() instanceof HeaderViewListAdapter) + result = ((HeaderViewListAdapter)getAdapter()).removeHeader(view); + if (result) + observer.onChanged(); + return result; + } public int getHeaderViewsCount() { - return 0; + return headerViews.size(); } public int getFooterViewsCount() { - return 0; + return footerViews.size(); } public void setDivider(Drawable drawable) {} public void setSelectionFromTop(int position, int y) {} - public void addFooterView(View v, Object data, boolean isSelectable) {} + public void addFooterView(View v, Object data, boolean isSelectable) { + footerViews.add(new FixedViewInfo(v, data, isSelectable)); + if (getAdapter() instanceof HeaderViewListAdapter) { + observer.onChanged(); + } else if (getAdapter() != null) { + setAdapter(getAdapter()); + } + } - public void addFooterView(View v) {} + public void addFooterView(View v) { + addFooterView(v, null, true); + } public void setDividerHeight(int height) {} }