1

我在对话框片段上使用 RecyclerView 和 StaggeredGridLayoutManager。它被称为搜索片段。

我的问题是当我更改数据(进行其他查询以从服务器获取其他数据)时,我在项目 0 和项目 1 之间的顶部有一个巨大的空白空间。如果我向上滚动,我将看到新数据,但之后应用程序崩溃。

日志猫:

java.lang.NullPointerException: Attempt to read from field 'int android.support.v7.widget.StaggeredGridLayoutManager$Span.mIndex' on a null object reference
                                                            at android.support.v7.widget.StaggeredGridLayoutManager.hasGapsToFix(StaggeredGridLayoutManager.java:344)
                                                            at android.support.v7.widget.StaggeredGridLayoutManager.checkForGaps(StaggeredGridLayoutManager.java:272)
                                                            at android.support.v7.widget.StaggeredGridLayoutManager.onScrollStateChanged(StaggeredGridLayoutManager.java:307)
                                                            at android.support.v7.widget.RecyclerView.dispatchOnScrollStateChanged(RecyclerView.java:3977)
                                                            at android.support.v7.widget.RecyclerView.setScrollState(RecyclerView.java:1219)
                                                            at android.support.v7.widget.RecyclerView.access$3900(RecyclerView.java:147)
                                                            at android.support.v7.widget.RecyclerView$ViewFlinger.run(RecyclerView.java:4128)
                                                            at android.view.Choreographer$CallbackRecord.run(Choreographer.java:777)
                                                            at android.view.Choreographer.doCallbacks(Choreographer.java:590)
                                                            at android.view.Choreographer.doFrame(Choreographer.java:559)
                                                            at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:763)
                                                            at android.os.Handler.handleCallback(Handler.java:739)
                                                            at android.os.Handler.dispatchMessage(Handler.java:95)
                                                            at android.os.Looper.loop(Looper.java:145)
                                                            at android.app.ActivityThread.main(ActivityThread.java:6897)
                                                            at java.lang.reflect.Method.invoke(Native Method)
                                                            at java.lang.reflect.Method.invoke(Method.java:372)
                                                            at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1404)
                                                            at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1199)

搜索片段:

public class SearchFragment extends BaseFragment implements SearchView.OnQueryTextListener, MusicPlayerService.MusicPlayerCallback {

public static final String KEY_SONGS = "KEY_SONGS";
public static final String KEY_ALBUMS = "KEY_ALBUMS";
public static final String KEY_VIDEOS = "KEY_VIDEOS";

private RecyclerView rcResult;
private StaggeredGridLayoutManager layoutManager;
private SearchAdapter searchAdapter;
private boolean gotPlayingItem;

@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setDefaultTitle("");
}

@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
    View view = inflater.inflate(R.layout.frm_search, container, false);
    rcResult = (RecyclerView) view.findViewById(R.id.rc_search_result);
    layoutManager = new StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL);
    rcResult.setLayoutManager(layoutManager);
    return view;
}

@Override
public void onResume() {
    super.onResume();
    getMainActivity().setSearchQueryListener(this);
    getThisApplication().getPlayerService().addMediaPlayerCallback(this);
}

@Override
public void onPause() {
    getMainActivity().setSearchQueryListener(null);
    getThisApplication().getPlayerService().removeMediaPlayerCallback(this);
    super.onPause();
}

@Override
public boolean onQueryTextSubmit(String query) {
    getMainActivity().clearSearchViewFocus();
    fetchData(query);
    return true;
}

@Override
public boolean onQueryTextChange(String newText) {
    return true;
}

private void fetchData(String query) {
    ProgressHUD.show(mContext, null, false, false, null);
    NetworkManager.searchKeyword(new Callback<SingerSearchResponse>() {
        @Override
        public void onResponse(Call<SingerSearchResponse> call, Response<SingerSearchResponse> response) {
            // TODO: Handle search response data
            KeyboardUtil.hideSoftKeyboard(getMainActivity());
            SingerSearchResponse singerSearchResponse = response.body();
            if (null == singerSearchResponse) {

            } else {
                SingerSearchResponse.SearchResponse searchResponse = singerSearchResponse.getSearchResponse();
                Map<String, List<? extends BaseDBModel>> searchResult = new HashMap<>();
                if (null != searchResponse) {
                    if (null == searchResponse.getSongs() || searchResponse.getSongs().size() == 0) {

                    } else {
                        searchResult.put(KEY_SONGS, searchResponse.getSongs());
                    }
                    if (null == searchResponse.getAlbums() || searchResponse.getAlbums().size() == 0) {

                    } else {
                        searchResult.put(KEY_ALBUMS, searchResponse.getAlbums());
                    }
                    if (null == searchResponse.getVideos() || searchResponse.getVideos().size() == 0) {

                    } else {
                        searchResult.put(KEY_VIDEOS, searchResponse.getVideos());
                    }
                    bindData(searchResult);
                }
            }
            ProgressHUD.dismissHUD();
        }

        @Override
        public void onFailure(Call<SingerSearchResponse> call, Throwable t) {
            ProgressHUD.dismissHUD();
        }
    }, query);
}

private void bindData(Map<String, List<? extends BaseDBModel>> searchResult) {
    searchAdapter = new SearchAdapter(getMainActivity(), searchResult);
    layoutManager.invalidateSpanAssignments();
    rcResult.setAdapter(searchAdapter);
}

@Override
public void onDestroy() {
    getMainActivity().closeSearchView();
    super.onDestroy();
}

@Override
public void onStartPlaying(BaseDBModel<? extends BaseDBModel> currentPlayingItem) {
    mUIThread.post(new Runnable() {
        @Override
        public void run() {
            gotPlayingItem = false;
            searchAdapter.notifyDataSetChanged();
        }
    });
}

@Override
public void onPlaying(BaseDBModel<? extends BaseDBModel> currentPlayingItem, int currentPositionMils) {
    if (!gotPlayingItem) {
        mUIThread.post(new Runnable() {
            @Override
            public void run() {
                gotPlayingItem = true;
                if (null != searchAdapter) {
                    searchAdapter.notifyDataSetChanged();
                }
            }
        });
    }
}

@Override
public void onPlayerResume() {

}

@Override
public void onPlayerPause() {

}

@Override
public void onPlayingCompleted() {
    mUIThread.post(new Runnable() {
        @Override
        public void run() {
            gotPlayingItem = false;
            searchAdapter.notifyDataSetChanged();
        }
    });
}

@Override
public void onError() {

}

@Override
public void onShuffle() {

}

搜索适配器

public class SearchAdapter extends RecyclerView.Adapter implements OnAdapterUpdate{

private final int TYPE_TITLE = 0;
private final int TYPE_SONG = 1;
private final int TYPE_ALBUM = 2;
private final int TYPE_VIDEO = 3;

private int POS_SONG_TITLE;
private int POS_ALBUM_TITLE;
private int POS_VIDEO_TITLE;

private MainActivity mainActivity;
private LayoutInflater layoutInflater;
private Map<String, List<? extends BaseDBModel>> data;
private List<? extends BaseDBModel> listSongs;
private List<? extends BaseDBModel> listAlbums;
private List<? extends BaseDBModel> listVideos;
private int totalRecords;

public SearchAdapter(MainActivity mainActivity, Map<String, List<? extends BaseDBModel>> data) {
    this.mainActivity = mainActivity;
    layoutInflater = (LayoutInflater) mainActivity.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    this.data = data;
    preparingData();
}

private void preparingData() {
    totalRecords = 0;
    POS_SONG_TITLE = POS_ALBUM_TITLE = POS_VIDEO_TITLE = -1;
    if (data.containsKey(SearchFragment.KEY_SONGS)) {
        listSongs = data.get(SearchFragment.KEY_SONGS);
        totalRecords += listSongs.size() + 1;
        POS_SONG_TITLE = 0;
        POS_ALBUM_TITLE = totalRecords;
    }
    if (data.containsKey(SearchFragment.KEY_ALBUMS)) {
        listAlbums = data.get(SearchFragment.KEY_ALBUMS);
        totalRecords += listAlbums.size() + 1;
        POS_VIDEO_TITLE = totalRecords;
    }
    if (data.containsKey(SearchFragment.KEY_VIDEOS)) {
        listVideos = data.get(SearchFragment.KEY_VIDEOS);
        totalRecords += listVideos.size() + 1;
    }
}

@Override
public int getItemViewType(int position) {
    if ((position == POS_SONG_TITLE && null != listSongs) || (position == POS_ALBUM_TITLE && null != listAlbums) || (position == POS_VIDEO_TITLE && null != listVideos)) {
        return TYPE_TITLE;
    } else if (null != listSongs && position > POS_SONG_TITLE && position <= listSongs.size()) {
        return TYPE_SONG;
    } else if (null != listAlbums && position > POS_ALBUM_TITLE && position <= POS_ALBUM_TITLE + listAlbums.size()) {
        return TYPE_ALBUM;
    } else if (null != listVideos && position > POS_VIDEO_TITLE && position <= totalRecords) {
        return TYPE_VIDEO;
    } else {
        return super.getItemViewType(position);
    }
}

private ICloseMoreAction iCloseMoreAction = new ICloseMoreAction() {
    @Override
    public void closeAllMoreMenu() {
        closeAllMenu();
    }
};

@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
    RecyclerView.ViewHolder viewHolder = null;
    View itemView = null;
    switch (viewType) {
        case TYPE_TITLE:
            itemView = layoutInflater.inflate(R.layout.item_title, parent, false);
            viewHolder = new TitleViewHolder(itemView);
            break;
        case TYPE_SONG:
            itemView = layoutInflater.inflate(R.layout.item_media_songs, parent, false);
            viewHolder = new SongViewHolder(itemView, mainActivity, iCloseMoreAction, this);
            break;
        case TYPE_ALBUM:
            itemView = layoutInflater.inflate(R.layout.item_media_album, parent, false);
            viewHolder = new AlbumViewHolder(itemView, mainActivity);
            break;
        case TYPE_VIDEO:
            itemView = layoutInflater.inflate(R.layout.item_media_videos, parent, false);
            viewHolder = new VideoViewHolder(itemView, mainActivity);
            break;
    }
    return viewHolder;
}

@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
    StaggeredGridLayoutManager.LayoutParams layoutParams = (StaggeredGridLayoutManager.LayoutParams) holder.itemView.getLayoutParams();
    if (null != listSongs && position > POS_SONG_TITLE && position < listSongs.size() + 1) {
        MediaSongModel mediaSongModel = (MediaSongModel) listSongs.get(position - 1);
        ((SongViewHolder) holder).bindData(mediaSongModel, null, BaseDBModel.TYPE_ONLINE, false);
        layoutParams.setFullSpan(true);
    } else if (null != listAlbums && position > POS_ALBUM_TITLE && position < POS_ALBUM_TITLE + listAlbums.size() + 1) {
        MediaAlbumModel mediaAlbumModel = (MediaAlbumModel) listAlbums.get(position - POS_ALBUM_TITLE - 1);
        ((AlbumViewHolder) holder).bindData(mediaAlbumModel, null);
    } else if (null != listVideos && position > POS_VIDEO_TITLE && position < totalRecords) {
        MediaVideoModel mediaVideoModel = (MediaVideoModel) listVideos.get(position - POS_VIDEO_TITLE - 1);
        ((VideoViewHolder) holder).bindData(mediaVideoModel, null);
    } else {
        if (POS_SONG_TITLE != -1 && position == POS_SONG_TITLE) {
            ((TitleViewHolder) holder).bindData("MP3");
        } else if (POS_ALBUM_TITLE != -1 && position == POS_ALBUM_TITLE) {
            ((TitleViewHolder) holder).bindData("Albums");
        } else if (POS_VIDEO_TITLE != -1 && position == POS_VIDEO_TITLE) {
            ((TitleViewHolder) holder).bindData("Videos");
        }
    }
}

@Override
public int getItemCount() {
    return totalRecords;
}

private void closeAllMenu() {
    int songListSize = listSongs.size();
    for (int i = 0; i < songListSize; i++) {
        if (((MediaSongModel) listSongs.get(i)).isExpanded()){
            ((MediaSongModel) listSongs.get(i)).setExpanded(false);
            notifyDataSetChanged();
            break;
        }
    }
}

@Override
public void updateAdapter() {
    notifyDataSetChanged();
}

@Override
public void removePosition(int position) {
    listSongs.remove(position);
    notifyItemRemoved(position);
}

public interface ICloseMoreAction {
    void closeAllMoreMenu();
}

}

请注意,我将每个项目高度更改为 wrap_content!

谢谢!

4

1 回答 1

0

在交换后备数据库后绑定到新游标时,我遇到了同样的问题。由于我的应用程序非常模块化,我还注意到在使用我支持的其他布局之一(使用 ListLayoutManager)时不会发生这种情况。

不知何故,这只发生在更改为空游标时(使适配器报告 0 个项目,它在其他点也这样做)然后在更改数据库时返回。在搜索之间注入空游标不会导致相同的问题,并且将适配器设置为报告不稳定的 id 也无济于事。这让我相信我的光标发生了一些奇怪的事情。但是根据上面的线索,我找到了一种解决方法,类似于我在运行时更改列数、布局和视图类型时所做的事情:当您知道会导致此类问题的情况出现时,只需重新创建 LayoutManager。

注意:我通过 LeakCanary 检查了旧的 LayoutManager 是否按预期收集,其他所有内容仍在使用中,因此没有内存泄漏。只有创建和设置 LayoutManager 的固有开销

这是我的适配器中的一个示例(为清楚起见进行了编辑):

public void receiveCursor(Cursor newCursor) {
    if(oldCursor == null && recyclerView.getLayoutManager() instanceof StaggeredGridLayoutManager){
        // there was a library switch before this, otherwise there would be
        // a non-null, empty, cursor
        setCursor(newCursor);

        // this is the pertinent exerpt from my re-setup code which is also called
        // from code that switches layouts etc.
        StaggeredGridLayoutManager manager = 
            new StaggeredGridLayoutManager(numCols, StaggeredGridLayoutManager.VERTICAL);
            manager.setGapStrategy(StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS);
            recyclerView.setLayoutManager(manager);
            recyclerView.setHasFixedSize(false);
    }else {
        setCursor(newCursor);
    }
}

我必须补充一点,交换后备数据库是一项相当罕见的任务,我非常愿意为一个简单的解决方案进行性能权衡。由于您的用例是在用户交互时可能每秒发生几次的搜索,因此此解决方案可能不适合您,因为在进行大量布局更改时对象创建、初始化和垃圾收集的开销。我只是想我会把这个记录下来,让其他人在谷歌上找到这个问题。

于 2016-07-21T21:24:04.990 回答