我的具体问题是:如何实现这样的效果:http://youtu.be/EJm7subFbQI
弹跳效果并不重要,但我需要标题的“粘性”效果。我从哪里开始?,我能以什么为基础?我需要一些可以在API 8上实现的东西。
感谢。
答案 0 :(得分:46)
此问题已存在一些解决方案。你所描述的是部分标题,并且在Android中被称为粘性部分标题。
答案 1 :(得分:6)
对于那些不想使用第三方代码(或不能直接使用它,例如在Xamarin中)的人来说,这可以通过手工轻松完成。 我们的想法是使用另一个ListView作为标题。此列表视图仅包含标题项。它不会被用户滚动(setEnabled(false)),但会根据主列表滚动代码。滚动。所以你将有两个列表 - headerListview和mainListview,以及两个相应的适配器headerAdapter和mainAdapter。 headerAdapter仅返回节视图,而mainAdapter支持两种视图类型(节和项)。您将需要一个方法,该方法在主列表中占据一个位置,并在区段列表中返回相应的位置。
主要活动
public class MainActivity extends AppCompatActivity {
public static final int TYPE_SECTION = 0;
public static final int TYPE_ITEM = 1;
ListView mainListView;
ListView headerListView;
MainAdapter mainAdapter;
HeaderAdapter headerAdapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mainListView = (ListView)findViewById(R.id.list);
headerListView = (ListView)findViewById(R.id.header);
mainAdapter = new MainAdapter();
headerAdapter = new HeaderAdapter();
headerListView.setEnabled(false);
headerListView.setAdapter(headerAdapter);
mainListView.setAdapter(mainAdapter);
mainListView.setOnScrollListener(new AbsListView.OnScrollListener(){
@Override
public void onScrollStateChanged(AbsListView view, int scrollState){
}
@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
// this should return an index in the headers list, based one the index in the main list. The logic for this is highly dependent on your data.
int pos = mainAdapter.getSectionIndexForPosition(firstVisibleItem);
// this makes sure our headerListview shows the proper section (the one on the top of the mainListview)
headerListView.setSelection(pos);
// this makes sure that headerListview is scrolled exactly the same amount as the mainListview
if(mainAdapter.getItemViewType(firstVisibleItem + 1) == TYPE_SECTION){
headerListView.setSelectionFromTop(pos, mainListView.getChildAt(0).getTop());
}
}
});
}
public class MainAdapter extends BaseAdapter{
int count = 30;
@Override
public int getItemViewType(int position){
if((float)position / 10 == (int)((float)position/10)){
return TYPE_SECTION;
}else{
return TYPE_ITEM;
}
}
@Override
public int getViewTypeCount(){ return 2; }
@Override
public int getCount() { return count - 1; }
@Override
public Object getItem(int position) { return null; }
@Override
public long getItemId(int position) { return position; }
public int getSectionIndexForPosition(int position){ return position / 10; }
@Override
public View getView(int position, View convertView, ViewGroup parent) {
View v = getLayoutInflater().inflate(R.layout.item, parent, false);
position++;
if(getItemViewType(position) == TYPE_SECTION){
((TextView)v.findViewById(R.id.text)).setText("SECTION "+position);
}else{
((TextView)v.findViewById(R.id.text)).setText("Item "+position);
}
return v;
}
}
public class HeaderAdapter extends BaseAdapter{
int count = 5;
@Override
public int getCount() { return count; }
@Override
public Object getItem(int position) { return null; }
@Override
public long getItemId(int position) { return position; }
@Override
public View getView(int position, View convertView, ViewGroup parent) {
View v = getLayoutInflater().inflate(R.layout.item, parent, false);
((TextView)v.findViewById(R.id.text)).setText("SECTION "+position*10);
return v;
}
}
}
这里有几点需要注意。我们不希望在主视图列表中显示第一部分,因为它会产生副本(它已经在标题中显示)。为避免这种情况,请在mainAdapter.getCount():
中return actualCount - 1;
并确保getView()方法的第一行是
position++;
这样你的主列表将渲染所有单元格,但第一个单元格。
另一件事是您要确保headerListview的高度与列表项的高度相匹配。在此示例中,高度是固定的,但如果您的项目高度未设置为dp中的精确值,则可能会非常棘手。请参阅此答案,了解如何解决此问题:https://stackoverflow.com/a/41577017/291688
主要布局
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin">
<ListView
android:id="@+id/header"
android:layout_width="match_parent"
android:layout_height="48dp"/>
<ListView
android:id="@+id/list"
android:layout_below="@+id/header"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</RelativeLayout>
项目/标题布局
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="48dp">
<TextView
android:id="@+id/text"
android:gravity="center_vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
/>
</LinearLayout>
答案 2 :(得分:1)
在app.gradle文件中添加
compile 'se.emilsjolander:StickyScrollViewItems:1.1.0'
然后我的布局,我已将android:tag ="sticky"
添加到特定视图(如textview或edittext而不是LinearLayout),如下所示。它也使用数据绑定,忽略它。
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable
name="temp"
type="com.lendingkart.prakhar.lendingkartdemo.databindingmodel.BusinessDetailFragmentModel" />
<variable
name="presenter"
type="com.lendingkart.prakhar.lendingkartdemo.presenters.BusinessDetailsPresenter" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.lendingkart.prakhar.lendingkartdemo.customview.StickyScrollView
android:id="@+id/sticky_scroll"
android:layout_width="match_parent"
android:layout_height="match_parent">
<!-- scroll view child goes here -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<android.support.v7.widget.CardView xmlns:card_view="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
card_view:cardCornerRadius="5dp"
card_view:cardUseCompatPadding="true">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
style="@style/group_view_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/businessdetailtitletextviewbackground"
android:padding="@dimen/activity_horizontal_margin"
android:tag="sticky"
android:text="@string/business_contact_detail" />
<android.support.design.widget.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="7dp">
<android.support.design.widget.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/comapnyLabel"
android:textSize="16sp" />
</android.support.design.widget.TextInputLayout>
<android.support.design.widget.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="5dp">
<android.support.design.widget.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/contactLabel"
android:textSize="16sp" />
</android.support.design.widget.TextInputLayout>
<android.support.design.widget.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="5dp">
<android.support.design.widget.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/emailLabel"
android:textSize="16sp" />
</android.support.design.widget.TextInputLayout>
<android.support.design.widget.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="5dp">
<android.support.design.widget.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/NumberOfEmployee"
android:textSize="16sp" />
</android.support.design.widget.TextInputLayout>
</LinearLayout>
</android.support.v7.widget.CardView>
<android.support.v7.widget.CardView xmlns:card_view="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
card_view:cardCornerRadius="5dp"
card_view:cardUseCompatPadding="true">
<TextView
style="@style/group_view_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/businessdetailtitletextviewbackground"
android:padding="@dimen/activity_horizontal_margin"
android:tag="sticky"
android:text="@string/nature_of_business" />
</android.support.v7.widget.CardView>
<android.support.v7.widget.CardView xmlns:card_view="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
card_view:cardCornerRadius="5dp"
card_view:cardUseCompatPadding="true">
<TextView
style="@style/group_view_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/businessdetailtitletextviewbackground"
android:padding="@dimen/activity_horizontal_margin"
android:tag="sticky"
android:text="@string/taxation" />
</android.support.v7.widget.CardView>
</LinearLayout>
</com.lendingkart.prakhar.lendingkartdemo.customview.StickyScrollView>
</LinearLayout>
</layout>
textview的样式组看起来像
<style name="group_view_text" parent="@android:style/TextAppearance.Medium">
<item name="android:layout_width">wrap_content</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:textColor">@color/edit_text_color</item>
<item name="android:textSize">16dp</item>
<item name="android:layout_centerVertical">true</item>
<item name="android:textStyle">bold</item>
</style>
textview的背景如下:(@ drawable / businessdetailtitletextviewbackground)
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape android:shape="rectangle">
<solid android:color="@color/edit_text_color" />
</shape>
</item>
<item android:bottom="2dp">
<shape android:shape="rectangle">
<solid android:color="@color/White" />
</shape>
</item>
</layer-list>
答案 3 :(得分:0)
您可以使用SuperSLiM库达到此效果。它为您提供了RecyclerView的LayoutManager,具有可互换的线性,网格和交错的视图显示。
一个好的演示位于github repository
只是得到这样的结果
app:slm_headerDisplay="inline|sticky"
or
app:slm_headerDisplay="sticky"
答案 4 :(得分:0)
我使用了一个特殊的类来实现类似iPhone的列表视图。 您可以在此处找到带有源代码的示例。 https://demonuts.com/android-recyclerview-sticky-header-like-iphone/
已更新列表视图的此类为
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.Gravity;
import android.view.View;
import android.view.animation.AlphaAnimation;
import android.widget.AbsListView;
import android.widget.AdapterView;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.ImageView.ScaleType;
import android.widget.ListView;
import android.widget.RelativeLayout;
public class HeaderListView extends RelativeLayout {
// TODO: Handle listViews with fast scroll
// TODO: See if there are methods to dispatch to mListView
private static final int FADE_DELAY = 1000;
private static final int FADE_DURATION = 2000;
private InternalListView mListView;
private SectionAdapter mAdapter;
private RelativeLayout mHeader;
private View mHeaderConvertView;
private FrameLayout mScrollView;
private AbsListView.OnScrollListener mExternalOnScrollListener;
public HeaderListView(Context context) {
super(context);
init(context, null);
}
public HeaderListView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context, attrs);
}
private void init(Context context, AttributeSet attrs) {
mListView = new InternalListView(getContext(), attrs);
LayoutParams listParams = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
listParams.addRule(ALIGN_PARENT_TOP);
mListView.setLayoutParams(listParams);
mListView.setOnScrollListener(new HeaderListViewOnScrollListener());
mListView.setVerticalScrollBarEnabled(false);
mListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
if (mAdapter != null) {
mAdapter.onItemClick(parent, view, position, id);
}
}
});
addView(mListView);
mHeader = new RelativeLayout(getContext());
LayoutParams headerParams = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
headerParams.addRule(ALIGN_PARENT_TOP);
mHeader.setLayoutParams(headerParams);
mHeader.setGravity(Gravity.BOTTOM);
addView(mHeader);
// The list view's scroll bar can be hidden by the header, so we display our own scroll bar instead
Drawable scrollBarDrawable = getResources().getDrawable(R.drawable.scrollbar_handle_holo_light);
mScrollView = new FrameLayout(getContext());
LayoutParams scrollParams = new LayoutParams(scrollBarDrawable.getIntrinsicWidth(), LayoutParams.MATCH_PARENT);
scrollParams.addRule(ALIGN_PARENT_RIGHT);
scrollParams.rightMargin = (int) dpToPx(2);
mScrollView.setLayoutParams(scrollParams);
ImageView scrollIndicator = new ImageView(context);
scrollIndicator.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
scrollIndicator.setImageDrawable(scrollBarDrawable);
scrollIndicator.setScaleType(ScaleType.FIT_XY);
mScrollView.addView(scrollIndicator);
mScrollView.setVisibility(INVISIBLE);
addView(mScrollView);
}
public void setAdapter(SectionAdapter adapter) {
mAdapter = adapter;
mListView.setAdapter(adapter);
}
public void setOnScrollListener(AbsListView.OnScrollListener l) {
mExternalOnScrollListener = l;
}
private class HeaderListViewOnScrollListener implements AbsListView.OnScrollListener {
private int previousFirstVisibleItem = -1;
private int direction = 0;
private int actualSection = 0;
private boolean scrollingStart = false;
private boolean doneMeasuring = false;
private int lastResetSection = -1;
private int nextH;
private int prevH;
private View previous;
private View next;
private AlphaAnimation fadeOut = new AlphaAnimation(1f, 0f);
private boolean noHeaderUpToHeader = false;
private boolean didScroll = false;
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
if (mExternalOnScrollListener != null) {
mExternalOnScrollListener.onScrollStateChanged(view, scrollState);
}
didScroll = true;
}
@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
if (mExternalOnScrollListener != null) {
mExternalOnScrollListener.onScroll(view, firstVisibleItem, visibleItemCount, totalItemCount);
}
if (!didScroll) {
return;
}
firstVisibleItem -= mListView.getHeaderViewsCount();
if (firstVisibleItem < 0) {
mHeader.removeAllViews();
return;
}
updateScrollBar();
if (visibleItemCount > 0 && firstVisibleItem == 0 && mHeader.getChildAt(0) == null) {
addSectionHeader(0);
lastResetSection = 0;
}
int realFirstVisibleItem = getRealFirstVisibleItem(firstVisibleItem, visibleItemCount);
if (totalItemCount > 0 && previousFirstVisibleItem != realFirstVisibleItem) {
direction = realFirstVisibleItem - previousFirstVisibleItem;
actualSection = mAdapter.getSection(realFirstVisibleItem);
boolean currIsHeader = mAdapter.isSectionHeader(realFirstVisibleItem);
boolean prevHasHeader = mAdapter.hasSectionHeaderView(actualSection - 1);
boolean nextHasHeader = mAdapter.hasSectionHeaderView(actualSection + 1);
boolean currHasHeader = mAdapter.hasSectionHeaderView(actualSection);
boolean currIsLast = mAdapter.getRowInSection(realFirstVisibleItem) == mAdapter.numberOfRows(actualSection) - 1;
boolean prevHasRows = mAdapter.numberOfRows(actualSection - 1) > 0;
boolean currIsFirst = mAdapter.getRowInSection(realFirstVisibleItem) == 0;
boolean needScrolling = currIsFirst && !currHasHeader && prevHasHeader && realFirstVisibleItem != firstVisibleItem;
boolean needNoHeaderUpToHeader = currIsLast && currHasHeader && !nextHasHeader && realFirstVisibleItem == firstVisibleItem && Math.abs(mListView.getChildAt(0).getTop()) >= mListView.getChildAt(0).getHeight() / 2;
noHeaderUpToHeader = false;
if (currIsHeader && !prevHasHeader && firstVisibleItem >= 0) {
resetHeader(direction < 0 ? actualSection - 1 : actualSection);
} else if ((currIsHeader && firstVisibleItem > 0) || needScrolling) {
if (!prevHasRows) {
resetHeader(actualSection-1);
}
startScrolling();
} else if (needNoHeaderUpToHeader) {
noHeaderUpToHeader = true;
} else if (lastResetSection != actualSection) {
resetHeader(actualSection);
}
previousFirstVisibleItem = realFirstVisibleItem;
}
if (scrollingStart) {
int scrolled = realFirstVisibleItem >= firstVisibleItem ? mListView.getChildAt(realFirstVisibleItem - firstVisibleItem).getTop() : 0;
if (!doneMeasuring) {
setMeasurements(realFirstVisibleItem, firstVisibleItem);
}
int headerH = doneMeasuring ? (prevH - nextH) * direction * Math.abs(scrolled) / (direction < 0 ? nextH : prevH) + (direction > 0 ? nextH : prevH) : 0;
mHeader.scrollTo(0, -Math.min(0, scrolled - headerH));
if (doneMeasuring && headerH != mHeader.getLayoutParams().height) {
LayoutParams p = (LayoutParams) (direction < 0 ? next.getLayoutParams() : previous.getLayoutParams());
p.topMargin = headerH - p.height;
mHeader.getLayoutParams().height = headerH;
mHeader.requestLayout();
}
}
if (noHeaderUpToHeader) {
if (lastResetSection != actualSection) {
addSectionHeader(actualSection);
lastResetSection = actualSection + 1;
}
mHeader.scrollTo(0, mHeader.getLayoutParams().height - (mListView.getChildAt(0).getHeight() + mListView.getChildAt(0).getTop()));
}
}
private void startScrolling() {
scrollingStart = true;
doneMeasuring = false;
lastResetSection = -1;
}
private void resetHeader(int section) {
scrollingStart = false;
addSectionHeader(section);
mHeader.requestLayout();
lastResetSection = section;
}
private void setMeasurements(int realFirstVisibleItem, int firstVisibleItem) {
if (direction > 0) {
nextH = realFirstVisibleItem >= firstVisibleItem ? mListView.getChildAt(realFirstVisibleItem - firstVisibleItem).getMeasuredHeight() : 0;
}
previous = mHeader.getChildAt(0);
prevH = previous != null ? previous.getMeasuredHeight() : mHeader.getHeight();
if (direction < 0) {
if (lastResetSection != actualSection - 1) {
addSectionHeader(Math.max(0, actualSection - 1));
next = mHeader.getChildAt(0);
}
nextH = mHeader.getChildCount() > 0 ? mHeader.getChildAt(0).getMeasuredHeight() : 0;
mHeader.scrollTo(0, prevH);
}
doneMeasuring = previous != null && prevH > 0 && nextH > 0;
}
private void updateScrollBar() {
if (mHeader != null && mListView != null && mScrollView != null) {
int offset = mListView.computeVerticalScrollOffset();
int range = mListView.computeVerticalScrollRange();
int extent = mListView.computeVerticalScrollExtent();
mScrollView.setVisibility(extent >= range ? View.INVISIBLE : View.VISIBLE);
if (extent >= range) {
return;
}
int top = range == 0 ? mListView.getHeight() : mListView.getHeight() * offset / range;
int bottom = range == 0 ? 0 : mListView.getHeight() - mListView.getHeight() * (offset + extent) / range;
mScrollView.setPadding(0, top, 0, bottom);
fadeOut.reset();
fadeOut.setFillBefore(true);
fadeOut.setFillAfter(true);
fadeOut.setStartOffset(FADE_DELAY);
fadeOut.setDuration(FADE_DURATION);
mScrollView.clearAnimation();
mScrollView.startAnimation(fadeOut);
}
}
private void addSectionHeader(int actualSection) {
View previousHeader = mHeader.getChildAt(0);
if (previousHeader != null) {
mHeader.removeViewAt(0);
}
if (mAdapter.hasSectionHeaderView(actualSection)) {
mHeaderConvertView = mAdapter.getSectionHeaderView(actualSection, mHeaderConvertView, mHeader);
mHeaderConvertView.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));
mHeaderConvertView.measure(MeasureSpec.makeMeasureSpec(mHeader.getWidth(), MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
mHeader.getLayoutParams().height = mHeaderConvertView.getMeasuredHeight();
mHeaderConvertView.scrollTo(0, 0);
mHeader.scrollTo(0, 0);
mHeader.addView(mHeaderConvertView, 0);
} else {
mHeader.getLayoutParams().height = 0;
mHeader.scrollTo(0, 0);
}
mScrollView.bringToFront();
}
private int getRealFirstVisibleItem(int firstVisibleItem, int visibleItemCount) {
if (visibleItemCount == 0) {
return -1;
}
int relativeIndex = 0, totalHeight = mListView.getChildAt(0).getTop();
for (relativeIndex = 0; relativeIndex < visibleItemCount && totalHeight < mHeader.getHeight(); relativeIndex++) {
totalHeight += mListView.getChildAt(relativeIndex).getHeight();
}
int realFVI = Math.max(firstVisibleItem, firstVisibleItem + relativeIndex - 1);
return realFVI;
}
}
public ListView getListView() {
return mListView;
}
public void addHeaderView(View v) {
mListView.addHeaderView(v);
}
private float dpToPx(float dp) {
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, getContext().getResources().getDisplayMetrics());
}
protected class InternalListView extends ListView {
public InternalListView(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected int computeVerticalScrollExtent() {
return super.computeVerticalScrollExtent();
}
@Override
protected int computeVerticalScrollOffset() {
return super.computeVerticalScrollOffset();
}
@Override
protected int computeVerticalScrollRange() {
return super.computeVerticalScrollRange();
}
}
}
XML用法
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<com.example.parsaniahardik.listview_stickyheader_ios.HeaderListView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/lv">
</com.example.parsaniahardik.listview_stickyheader_ios.HeaderListView>