如何制作与 RecyclerView 兼容的 Android 自定义视图

How to make an Android custom view that is compatible with RecyclerView

我创建了一个仅扩展视图 class 的自定义视图。自定义视图工作完美,除非在 RecyclerView 中使用。这是自定义视图:

public class KdaBar extends View {
    private int mKillCount, mDeathCount, mAssistCount;
    private int mKillColor, mDeathColor, mAssistColor;
    private int mViewWidth, mViewHeight;
    private Paint mKillBarPaint, mDeathBarPaint, mAssistBarPaint, mBgPaint;
    private float mKillPart, mDeathPart, mAssistPart;

    public KdaBar(Context context, AttributeSet attrs) {
        super(context, attrs);

        TypedArray a = context.getTheme().obtainStyledAttributes(
                attrs,
                R.styleable.KdaBar,
                0, 0);

        try {
            mKillCount = a.getInt(R.styleable.KdaBar_killCount, 0);
            mDeathCount = a.getInt(R.styleable.KdaBar_deathCount, 0);
            mAssistCount = a.getInt(R.styleable.KdaBar_assistCount, 0);

            mKillColor = a.getColor(R.styleable.KdaBar_killBarColor, ContextCompat.getColor(getContext(), R.color.kill_score_color));
            mDeathColor = a.getColor(R.styleable.KdaBar_deathBarColor, ContextCompat.getColor(getContext(), R.color.death_score_color));
            mAssistColor = a.getColor(R.styleable.KdaBar_assistBarColor, ContextCompat.getColor(getContext(), R.color.assist_score_color));
        } finally {
            a.recycle();
        }

        init();
    }

    public void setValues(int killCount, int deathCount, int assistCount) {

        mKillCount = killCount;
        mDeathCount = deathCount;
        mAssistCount = assistCount;

        invalidate();
    }

    @Override
    public void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        canvas.drawRect(0f, 0f, mViewWidth, mViewHeight, mBgPaint);
        canvas.drawRect(mKillPart+mDeathPart, 0f, mKillPart+mDeathPart+mAssistPart, mViewHeight, mAssistBarPaint);
        canvas.drawRect(mKillPart, 0f, mKillPart+mDeathPart, mViewHeight, mDeathBarPaint);
        canvas.drawRect(0f, 0f, mKillPart, mViewHeight, mKillBarPaint);
    }

    @Override
    protected void onSizeChanged(int xNew, int yNew, int xOld, int yOld){
        super.onSizeChanged(xNew, yNew, xOld, yOld);

        mViewWidth = xNew;
        mViewHeight = yNew;

        float total = mKillCount + mDeathCount + mAssistCount;
        mKillPart = (mKillCount/total) * mViewWidth;
        mDeathPart = (mDeathCount/total) * mViewWidth;
        mAssistPart = (mAssistCount/total) * mViewWidth;
    }

    private void init() {
        mKillBarPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mKillBarPaint.setColor(mKillColor);

        mDeathBarPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mDeathBarPaint.setColor(mDeathColor);

        mAssistBarPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mAssistBarPaint.setColor(mAssistColor);

        mBgPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mBgPaint.setColor(ContextCompat.getColor(getContext(), R.color.transparent));
    }
}

链接的图像是自定义视图当前的样子(自定义视图是中心数字上方的矩形)http://imgur.com/a/Ib5Yl

该栏下方的数字表示它们的值(它们用颜色编码以防您没有注意到)。很明显,第一项的值为零不应在自定义视图上显示蓝色条。奇怪,我知道。

下面的方法是设置值的地方(它在 RecyclerView.Adapter<> 内):

@Override
public void onBindViewHolder(ViewHolder holder, int position) {
    MatchHistory.Match item = mDataset.get(position);
    MatchHistory.MatchPlayer[] players = item.getPlayers();

    for(MatchHistory.MatchPlayer player: players) {
        int steamId32 = (int) Long.parseLong(mCurrentPlayer.getSteamId());
        if (steamId32 == player.getAccountId()) {
            mCurrentMatchPlayer = player;
        }
    }
    ...
    holder.mKdaBar.setValues(mCurrentMatchPlayer.getKills(), mCurrentMatchPlayer.getDeaths(), mCurrentMatchPlayer.getAssists());
    ...
}

这是 onCreateViewHolder:

@Override
public MatchesAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
    View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.fragment_match_item, parent, false);
    ViewHolder vh = new ViewHolder(v);
    return vh;
}

和 ViewHolder class:

public static class ViewHolder extends RecyclerView.ViewHolder {
    KdaBar mKdaBar;

    public ViewHolder(View v) {
        super(v);
        ...
        mKdaBar = (KdaBar) v.findViewById(R.id.kda_bar);
        ...
    }
}

我认为注意适配器使用的数据集会不时更改项目的位置是很有用的(因为它是同时获取的,但被插入以便对数据集进行排序).我差点忘了我也测试了不改变数据集中项目的位置,但仍然没有任何好的结果。如果您检查了图像,您可以看到项目中还有其他信息,我 100% 确定除了自定义视图中的数据外,这些都是正确的。

我想我忘记了一些必须重写的方法,但我已经看过很多教程,其中 none 提到了这个问题。期待解决这个问题。 TIA!

很难说清楚究竟发生了什么,尤其是如果这段代码在其他地方也能正常工作的话,但我会进行一些猜测。

我注意到的主要事情:

  1. 比较 int 和 long,其中数字非常接近 max
  2. 从 RecyclerView 中的视图调用 Invalidate(尤其是 onBindView)

第 1 期

在你的图片中,我猜你是 steamId,它是每个 RecyclerView 视图持有者左下角的数字,例如:'2563966339'。你应该知道Android中的“通常”,Integer.MAX_VALUE = 2147483647。这几乎意味着你应该使用 long 否则当你认为它们是... (所以也许盒子被正确绘制,但你只是不认为位置 0 的 steamId是你认为的那个人吗?!?!)。

(如果您想了解更多信息,只需查看 int 和 long 的有符号字节与无符号字节)。

因此您可能需要更改一些代码,但我建议使用 long 或 Long。下面的许多可能性中的两个

示例 1

long steamId32 = Long.parseLong(mCurrentPlayer.getSteamId());
if (steamId32 == player.getAccountId()) {
    mCurrentMatchPlayer = player;
}

示例 2

Long steamId32 = mCurrentPlayer.getSteamId();
if (steamId32.equals(player.getAccountId()) {
    mCurrentMatchPlayer = player;
}

问题 2:

不了解 RecyclerView 的工作原理可能会导致一些问题。在 onBindView 中,您应该尽可能地设置和绘制视图(不调用 invalidate())。这是因为 RecyclerView 旨在处理所有 'recycling'。所以你 invalidate() 调用可能会导致一些奇怪的问题。

我知道通常不会在每次绑定视图时调用 onDraw(),而是仅在使用 RecyclerView 创建时调用。这可以解释为什么它在其他地方有效!

总结与分析:

1号:

我会打电话给(在 setValues 之前 onBindView 内)

Log.d("Whatever", "At position: " + position + " we have " + <steamId> + <kills> + <other desired info>).

向上和向下滚动后,您会看到顶部的人以及正在调用的值,看看这是#1 中提到的问题还是您的位置有问题。如果这个人应该有0,那么让位置0显示0击杀。

这也可以指出我认为不太可能但绝对有可能出现的这些问题之一:

我仍然不知道 mCurrentPlayer 到底是什么,这可能会导致问题。此外,如果您需要更新适配器中的 'item',只需使用 recyclerView 从 Activity/Fragment 调用 mAdapter.updateItemAt(position)。如果您必须移动它,请调用 mAdapter.notifyItemMoved(fromPos, toPos)。所有这些都意味着当调用 onBindView 时,事情可能不是你想的那样。

2号:

我建议将 Log 语句也放在 onDraw() 中,以查看您是否知道何时 ACTUALLY 被调用,而不仅仅是在 [=13= 之后期待它].很可能 invaidate() 被主线程/回收器视图 排队 直到它决定要调用 onDraw().

(因为它已经created/drew onCreateView()中的项目)

您可能会对 RecyclerView、LayoutManager 和 Adapter 的作用以及它们调用视图方法的方式感到惊讶。 (你可能也只是想在 onBindViewonCreateView 中放置 Log 语句以了解 onDraw() 的整个过程)。

了解 RecyclerView(及其组成部分)

学习基础知识的视频:

对于读者,Android 文档提供了以下摘要:

Adapter: A subclass of RecyclerView.Adapter responsible for providing views that represent items in a data set.
Position: The position of a data item within an Adapter.
Index: The index of an attached child view as used in a call to getChildAt(int). Contrast with Position.
Binding: The process of preparing a child view to display data corresponding to a position within the adapter.
Recycle (view): A view previously used to display data for a specific adapter position may be placed in a cache for later reuse to display the same type of data again later. This can drastically improve performance by skipping initial layout inflation or construction.
Scrap (view): A child view that has entered into a temporarily detached state during layout. Scrap views may be reused without becoming fully detached from the parent RecyclerView, either unmodified if no rebinding is required or modified by the adapter if the view was considered dirty.
Dirty (view): A child view that must be rebound by the adapter before being displayed.

问题不在于数据集,而在于我对 RecyclerView 如何在底层工作的理解(正如 napkinsterror 在他的回答中提到的那样)。

这是修改后的自定义视图:

public class KdaBar extends View {
    private int mKillCount, mDeathCount, mAssistCount;
    private int mKillColor, mDeathColor, mAssistColor;
    private int mViewWidth, mViewHeight;
    private Paint mKillBarPaint, mDeathBarPaint, mAssistBarPaint, mBgPaint;
    private float mKillPart, mDeathPart, mAssistPart;

    public KdaBar(Context context, AttributeSet attrs) {
        super(context, attrs);

        TypedArray a = context.getTheme().obtainStyledAttributes(
                attrs,
                R.styleable.KdaBar,
                0, 0);

        try {
            mKillCount = a.getInt(R.styleable.KdaBar_killCount, 0);
            mDeathCount = a.getInt(R.styleable.KdaBar_deathCount, 0);
            mAssistCount = a.getInt(R.styleable.KdaBar_assistCount, 0);

            mKillColor = a.getColor(R.styleable.KdaBar_killBarColor, ContextCompat.getColor(getContext(), R.color.kill_score_color));
            mDeathColor = a.getColor(R.styleable.KdaBar_deathBarColor, ContextCompat.getColor(getContext(), R.color.death_score_color));
            mAssistColor = a.getColor(R.styleable.KdaBar_assistBarColor, ContextCompat.getColor(getContext(), R.color.assist_score_color));
        } finally {
            a.recycle();
        }

        init();
    }

    public void setValues(int killCount, int deathCount, int assistCount) {
        mKillCount = killCount;
        mDeathCount = deathCount;
        mAssistCount = assistCount;
    }

    private void calculatePartitions() {
        float total = mKillCount + mDeathCount + mAssistCount;
        mKillPart = (mKillCount/total) * mViewWidth;
        mDeathPart = (mDeathCount/total) * mViewWidth;
        mAssistPart = (mAssistCount/total) * mViewWidth;
    }

    @Override
    public void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        calculatePartitions();

        canvas.drawRect(mKillPart+mDeathPart, 0f, mKillPart+mDeathPart+mAssistPart, mViewHeight, mAssistBarPaint);
        canvas.drawRect(mKillPart, 0f, mKillPart+mDeathPart, mViewHeight, mDeathBarPaint);
        canvas.drawRect(0f, 0f, mKillPart, mViewHeight, mKillBarPaint);
    }

    @Override
    protected void onSizeChanged(int xNew, int yNew, int xOld, int yOld){
        super.onSizeChanged(xNew, yNew, xOld, yOld);

        mViewWidth = xNew;
        mViewHeight = yNew;
    }

    private void init() {
        mKillBarPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mKillBarPaint.setColor(mKillColor);

        mDeathBarPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mDeathBarPaint.setColor(mDeathColor);

        mAssistBarPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mAssistBarPaint.setColor(mAssistColor);

        mBgPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mBgPaint.setColor(ContextCompat.getColor(getContext(), R.color.transparent));
    }
}

这些是我所做的更改:

  1. setValues() 中删除了 invalidate() 调用,因为 parent 添加视图时会调用 onDraw() 回调。
  2. mKillPartmDeathPartmAssistPart 的赋值移动到 calculatePartitions(),后者又在 onDraw() 中调用。这是因为保证计算所需的值在onDraw()内是完整的。这将在下面解释。

这是我从 napkinsterror 先生的回答中收集到的内容:

当 LayoutManager 向 RecyclerView 请求视图时,最终会调用 onBindViewHolder() 方法。在该方法中,数据绑定到视图,因此调用 setValues()

视图返回到 LayoutManager,然后将项目添加回 RecyclerView。此事件将触发 onSizeChanged(),因为视图的尺寸尚不清楚。这就是检索 mViewWidthmViewHeight 的地方。至此,calculatePartitions() 的所有必要值都已完成。

onDraw() 也被调用,因为 parent 刚刚添加了一个项目(检查这个 image)。 calculatePartitions()onDraw() 中调用,视图将毫无问题地绘制在 canvas 上。

我之前得到错误值的原因是因为我在 onSizeChanged() 中做了 calculatePartitions() 这是非常非常错误的,因为 mViewWidthmViewHeight 还没有已知。

我会将此标记为答案,但非常感谢先生。 napkinsterror 提供资源,以便我可以在正确的方向上进行研究。 :)