如何模仿棒棒糖联系人应用程序上的 listView 粘性项目?

Posted

技术标签:

【中文标题】如何模仿棒棒糖联系人应用程序上的 listView 粘性项目?【英文标题】:How to mimic the listView stickey-items like on Lollipop's contacts app? 【发布时间】:2014-12-23 13:32:23 【问题描述】:

背景

Google 最近发布了一个新版本的 android,它有一个如下所示的联系人应用:

问题

我需要模仿这种列表,但我不知道如何做好。

这包括 3 个主要部分:

    粘性标题,只要顶部联系人的姓名以此字母开头。

    圆形照片或圆形+字母(适用于没有照片的联系人)

    PagerTitleStrip,它在项目之间具有统一的间距,并尝试在屏幕上显示所有项目(或在需要时允许滚动)。

我发现了什么

    有很多第三方库,但它们会处理stickey-headers,它们是listView 本身的项目的一部分。它们的标题不在左侧,而是在项目的顶部。

    在这里,左侧的移动方式与右侧不同。它可以粘贴,如果不需要粘贴(例如,因为该部分有一个项目),它会滚动。

    我注意到对于圆形照片,我可以使用名为 "RoundedBitmapDrawableFactory" 的新 (?) 类(创建 "RoundedBitmapDrawable" )。这可能会让我把一张照片很好地做成圆形。但是,它不适用于字母(用于没有照片的联系人),但我认为我可以将 textView 放在顶部,并将背景设置为颜色。

    另外,我注意到,为了很好地使用“RoundedBitmapDrawable”(使其真正成为圆形),我必须为其提供一个正方形大小的位图。否则,它的形状会很奇怪。

    我尝试使用“setTextSpacing”来最小化项目之间的空间,但它似乎不起作用。我也找不到任何方法来设置/自定义 PagerTitleStrip 的样式,使其与联系人应用程序一样。

    我也尝试过使用 "PagerTabStrip" ,但也没有用。

问题

您如何模仿 Google 实现此屏幕的方式?

更具体地说:

    如何使左侧的行为与联系人应用程序一样?

    这是实现圆形照片的最佳方式吗?在使用特殊的drawable之前,我真的必须将位图裁剪成正方形吗?是否有关于使用哪种颜色的设计指南?有没有更正式的方式来使用圆形文本单元格?

    您如何设置 PagerTitleStrip 的样式以使其具有与联系人应用程序相同的外观?


Github 项目

编辑:对于#1 和#2,我在 Github 上做了一个项目,here。遗憾的是我还发现了一个重要的错误,所以我也发布了它here

【问题讨论】:

我也必须在我的应用程序中开发您问题的第一部分。也许你可以看看 PinnedHeaderListView 类 - android.googlesource.com/platform/packages/apps/ContactsCommon/… @androidGuy 找到了所有这些问题的解决方案,尽管它们都不是很正式的...... 对于该问题的未来读者,我最近为 Android 分叉了一个粘性标头存储库,并将其设置为如上所述的“粘性侧索引”。有关代码/实现,请参阅 GitHub 上的 sticky-alphabet-index。 【参考方案1】:

好的,我已经设法解决了我所写的所有问题:

1.我改变了第三方库的工作方式(我不记得我从哪里得到库,但this one 非常相似),通过改变每一行的布局,以便标题位于内容本身的左侧。这只是一个布局 XML 文件的问题,您已经完成了很多工作。也许我会为这两种解决方案发布一个不错的库。

2.这是我的观点。这不是官方的实现(没有找到),所以我自己做了一些东西。它可以更高效,但至少它很容易理解并且相当灵活:

public class CircularView extends ViewSwitcher 
    private ImageView mImageView;
    private TextView mTextView;
    private Bitmap mBitmap;
    private CharSequence mText;
    private int mBackgroundColor = 0;
    private int mImageResId = 0;

    public CircularView(final Context context) 
        this(context, null);
    

    public CircularView(final Context context, final AttributeSet attrs) 
        super(context, attrs);
        addView(mImageView = new ImageView(context), new FrameLayout.LayoutParams(LayoutParams.MATCH_PARENT,
                LayoutParams.MATCH_PARENT, Gravity.CENTER));
        addView(mTextView = new TextView(context), new FrameLayout.LayoutParams(LayoutParams.MATCH_PARENT,
                LayoutParams.MATCH_PARENT, Gravity.CENTER));
        mTextView.setGravity(Gravity.CENTER);
        if (isInEditMode())
            setTextAndBackgroundColor("", 0xFFff0000);
    

    @Override
    protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) 
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        final int measuredWidth = getMeasuredWidth();
        final int measuredHeight = getMeasuredHeight();
        if (measuredWidth != 0 && measuredHeight != 0)
            drawContent(measuredWidth, measuredHeight);
    

    @SuppressWarnings("deprecation")
    private void drawContent(final int measuredWidth, final int measuredHeight) 
        ShapeDrawable roundedBackgroundDrawable = null;
        if (mBackgroundColor != 0) 
            roundedBackgroundDrawable = new ShapeDrawable(new OvalShape());
            roundedBackgroundDrawable.getPaint().setColor(mBackgroundColor);
            roundedBackgroundDrawable.setIntrinsicHeight(measuredHeight);
            roundedBackgroundDrawable.setIntrinsicWidth(measuredWidth);
            roundedBackgroundDrawable.setBounds(new Rect(0, 0, measuredWidth, measuredHeight));
        
        if (mImageResId != 0) 
            mImageView.setBackgroundDrawable(roundedBackgroundDrawable);
            mImageView.setImageResource(mImageResId);
            mImageView.setScaleType(ScaleType.CENTER_INSIDE);
         else if (mText != null) 
            mTextView.setText(mText);
            mTextView.setBackgroundDrawable(roundedBackgroundDrawable);
            // mTextView.setPadding(0, measuredHeight / 4, 0, measuredHeight / 4);
            mTextView.setTextSize(measuredHeight / 5);
         else if (mBitmap != null) 
            mImageView.setScaleType(ScaleType.FIT_CENTER);
            mImageView.setBackgroundDrawable(roundedBackgroundDrawable);
            mBitmap = ThumbnailUtils.extractThumbnail(mBitmap, measuredWidth, measuredHeight);
            final RoundedBitmapDrawable roundedBitmapDrawable = RoundedBitmapDrawableFactory.create(getResources(),
                    mBitmap);
            roundedBitmapDrawable.setCornerRadius((measuredHeight + measuredWidth) / 4);
            mImageView.setImageDrawable(roundedBitmapDrawable);
        
        resetValuesState(false);
    

    public void setTextAndBackgroundColor(final CharSequence text, final int backgroundColor) 
        resetValuesState(true);
        while (getCurrentView() != mTextView)
            showNext();
        this.mBackgroundColor = backgroundColor;
        mText = text;
        final int height = getHeight(), width = getWidth();
        if (height != 0 && width != 0)
            drawContent(width, height);
    

    public void setImageResource(final int imageResId, final int backgroundColor) 
        resetValuesState(true);
        while (getCurrentView() != mImageView)
            showNext();
        mImageResId = imageResId;
        this.mBackgroundColor = backgroundColor;
        final int height = getHeight(), width = getWidth();
        if (height != 0 && width != 0)
            drawContent(width, height);
    

    public void setImageBitmap(final Bitmap bitmap) 
        setImageBitmapAndBackgroundColor(bitmap, 0);
    

    public void setImageBitmapAndBackgroundColor(final Bitmap bitmap, final int backgroundColor) 
        resetValuesState(true);
        while (getCurrentView() != mImageView)
            showNext();
        this.mBackgroundColor = backgroundColor;
        mBitmap = bitmap;
        final int height = getHeight(), width = getWidth();
        if (height != 0 && width != 0)
            drawContent(width, height);
    

    private void resetValuesState(final boolean alsoResetViews) 
        mBackgroundColor = mImageResId = 0;
        mBitmap = null;
        mText = null;
        if (alsoResetViews) 
            mTextView.setText(null);
            mTextView.setBackgroundDrawable(null);
            mImageView.setImageBitmap(null);
            mImageView.setBackgroundDrawable(null);
        
    

    public ImageView getImageView() 
        return mImageView;
    

    public TextView getTextView() 
        return mTextView;
    


3.我找到了一个很好的库,叫做 PagerSlidingTabStrip 。不过,没有找到一种官方的方式来设置原生样式的样式。

另一种方法是查看 Google 的示例,该示例可直接在 Android-Studio 中使用,称为“SlidingTabLayout”。它显示了它是如何完成的。

编辑:#3 更好的库是here,也称为“PagerSlidingTabStrip”。

【讨论】:

【参考方案2】:

您可以执行以下操作:

    在 RecyclerView 的最左侧,创建一个保存字母索引的 TextView; 在 Recycler 视图的顶部(在包装它的布局中)放置一个 TextView 以覆盖您在第 1 步中创建的那个,这将是粘性的; 在您的 RecyclerView 中添加一个 OnScrollListener。在 onScrolled() 方法上,将第 2 步中创建的 TextView 设置为取自 firstVisibleRow 的参考文本。在此之前,您将拥有一个粘性索引,没有过渡的影响;

    要添加淡入/淡出过渡效果,请开发一个逻辑来检查 currentFirstVisibleItem 的前一个项目是否是前一个字母列表的最后一个,或者 secondVisibleItem 是否是新字母的第一个。基于这些信息,使粘性索引可见/不可见,而行索引相反,最后添加了 alpha 效果。

       if (recyclerView != null) 
        View firstVisibleView = recyclerView.getChildAt(0);
        View secondVisibleView = recyclerView.getChildAt(1);
    
        TextView firstRowIndex = (TextView) firstVisibleView.findViewById(R.id.sticky_row_index);
        TextView secondRowIndex = (TextView) secondVisibleView.findViewById(R.id.sticky_row_index);
    
        int visibleRange = recyclerView.getChildCount();
        int actual = recyclerView.getChildPosition(firstVisibleView);
        int next = actual + 1;
        int previous = actual - 1;
        int last = actual + visibleRange;
    
        // RESET STICKY LETTER INDEX
        stickyIndex.setText(String.valueOf(getIndexContext(firstRowIndex)).toUpperCase());
        stickyIndex.setVisibility(TextView.VISIBLE);
    
        if (dy > 0) 
            // USER SCROLLING DOWN THE RecyclerView
            if (next <= last) 
                if (isHeader(firstRowIndex, secondRowIndex)) 
                    stickyIndex.setVisibility(TextView.INVISIBLE);
                    firstRowIndex.setVisibility(TextView.VISIBLE);
                    firstRowIndex.setAlpha(1 - (Math.abs(firstVisibleView.getY()) / firstRowIndex.getHeight()));
                    secondRowIndex.setVisibility(TextView.VISIBLE);
                 else 
                    firstRowIndex.setVisibility(TextView.INVISIBLE);
                    stickyIndex.setVisibility(TextView.VISIBLE);
                
            
         else 
            // USER IS SCROLLING UP THE RecyclerVIew
            if (next <= last) 
                // RESET FIRST ROW STATE
                firstRowIndex.setVisibility(TextView.INVISIBLE);
    
                if ((isHeader(firstRowIndex, secondRowIndex) || (getIndexContext(firstRowIndex) != getIndexContext(secondRowIndex))) && isHeader(firstRowIndex, secondRowIndex)) 
                    stickyIndex.setVisibility(TextView.INVISIBLE);
                    firstRowIndex.setVisibility(TextView.VISIBLE);
                    firstRowIndex.setAlpha(1 - (Math.abs(firstVisibleView.getY()) / firstRowIndex.getHeight()));
                    secondRowIndex.setVisibility(TextView.VISIBLE);
                 else 
                    secondRowIndex.setVisibility(TextView.INVISIBLE);
                
            
        
    
        if (stickyIndex.getVisibility() == TextView.VISIBLE) 
            firstRowIndex.setVisibility(TextView.INVISIBLE);
        
    
    

我已经开发了一个执行上述逻辑的组件,可以在这里找到:https://github.com/edsilfer/sticky-index

【讨论】:

您的演示应用有很多错误,请修复它 这里有效,你能明确地说你发现了哪些错误【参考方案3】:

我这么晚才回答,但我希望我的回答对某人有所帮助。我也有这样的任务。我一直在寻找粘性标题的答案和示例,但对于 recyclerView。我在 Sabre Solooki 的文章"Sticky Header For RecyclerView" 中找到了最好最简单的解决方案。

基于这个例子,我为我的应用制作了我的联系人模块,非常简单。

【讨论】:

【参考方案4】:

应用程序的源代码总是一个很好的起点

https://github.com/android/platform_packages_apps_contacts/tree/master/src/com/android/contacts https://android.googlesource.com/platform/packages/apps/Contacts/

【讨论】:

这很好,但它确实需要很长时间,遗憾的是我没有。你以前看过那里的代码吗?你能指出我写的每一件事的具体实现位置吗? 我可以把你指向适配器android.googlesource.com/platform/packages/apps/ContactsCommon/… 我不确定这是否有帮助。我没有看到我在那里写的任何东西。 :( 在里面你有bindSectionHeaderAndDivider,如果你在层次结构中上一层到ContactListAdapter,你会发现有问题的方法。剩下的就看你自己了。 我想我已经找到了关于 listView 本身的解决方案。现在剩下的就是 PagerTitleStrip【参考方案5】:

要实现类似于电话联系人列表的功能,This 是我遇到的最佳解决方案!

这也适用于自定义列表,因为图像可能需要与名称一起存在。以上解决方案只有String的列表或数组,可能不是每个人都需要!

【讨论】:

似乎有问题:github.com/timusus/RecyclerView-FastScroll/issues/97

以上是关于如何模仿棒棒糖联系人应用程序上的 listView 粘性项目?的主要内容,如果未能解决你的问题,请参考以下文章

如何从 onItemClicklistener 上的联系人 ListView 获取 phone_Number

单击 ListView 上的侦听器

Android:如何像在联系人应用程序中一样使用字母索引器和快速滚动填充 ListView。

webview上的Html5视频缺少棒棒糖上的全屏按钮

棒棒糖设备上的 EditText 没有以强调色着色

如何使用 ListView 删除 Android Studio 显示中的重复联系人 [重复]