获取数据,自动加载更多

作者:计算机网络

1.熟悉RecyclerView2.自动加载更多,作为每个应用开发过程当中几乎必不可少的功能,了解如何实现,然后做一些封装,很有必要

很多时候,项目中都会有列表加载更多的场景,这次我们让RecyclerView轻松拥有加载更多的功能。虽然已有许多类似的轮子,但有的功能过于复杂,其实很多都用不到,所以不妨打造更适合自己的轮子。

这篇文章分享我的 Android 开发(入门)课程 的第七个和第八个实战项目:书籍列表应用和新闻应用。这两个项目都托管在我的 GitHub 上,分别是 BookListing 和 NewsApp 这两个 Repository,项目介绍已详细写在 README 上,欢迎大家 star 和 fork。

原创 2017-07-27 认真的 小苏

我们先来分析一下,什么时候需要用到加载更多的功能?自然是用户向上翻动列表,快要到所有列表项的底部,或则已经到底部的时候需要加载更多的数据,用于展示给用户(为了更好的用户体验,肯定是用户没有到最低端的时候就自动加载更多啦)。那么问题来了,怎么样能知道快要到底部,或者已经到底部了?翻查Android 文档

我们的RecyclerView加载更多是通过其Adapter子类实现的,接下来我们一步步的构建Adapter吧!

这两个实战项目的主要目的是练习从 Web API 获取应用数据,不过在实际 coding 的过程中使用了很多其它有意思的 Android 组件,这篇文章就逐个分享给大家。文章内容不会按应用的开发流程进行,各部分内容相对独立,大家可以利用浏览器的查找 (cmd/ctrl F) 功能按需取用。为了精简篇幅,文中的代码有删减,请以 GitHub 中的代码为准。

图片 1

 void addOnScrollListener (RecyclerView.OnScrollListener listener) 

1、编写通用的Adapter、ViewHolder

一般情况下使用Adapter都要为其创建一个ViewHolder,既然要编写通用的Adapter,首先要有一个通用的ViewHolder:

public class ViewHolder extends RecyclerView.ViewHolder {
    private SparseArray<View> mViews;
    private View mConvertView;

    private ViewHolder(View itemView) {
        super(itemView);
        mConvertView = itemView;
        mViews = new SparseArray<>();
    }

    public static ViewHolder create(Context context, int layoutId, ViewGroup parent) {
        View itemView = LayoutInflater.from(context).inflate(layoutId, parent, false);
        return new ViewHolder(itemView);
    }

    public static ViewHolder create(View itemView) {
        return new ViewHolder(itemView);
    }

    public <T extends View> T getView(int viewId) {
        View view = mViews.get(viewId);
        if (view == null) {
            view = mConvertView.findViewById(viewId);
            mViews.put(viewId, view);
        }
        return (T) view;
    }

    public View getConvertView() {
        return mConvertView;
    }

    public void setText(int viewId, String text) {
        TextView textView = getView(viewId);
        textView.setText(text);
    }
    .......省略其它辅助方法.........
}

我们自定义的ViewHolder类可以根据布局文件的id或具体的itemView返回一个ViewHolder对象,并用SparseArray来缓存我们itemView中的子View,避免每次都要去解析子View,同时提供相关辅助方法设置itemView的内容。有了ViewHolder,接下来编写Adapter就简单了:

public abstract class BaseAdapter<T> extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
    public static final int TYPE_COMMON_VIEW = 100001;

    private OnItemClickListeners<T> mItemClickListener;

    protected Context mContext;
    protected List<T> mDatas;

    protected abstract void convert(ViewHolder holder, T data);

    protected abstract int getItemLayoutId();

    public BaseAdapter(Context context, List<T> datas) {
        mContext = context;
        mDatas = datas == null ? new ArrayList<T>() : datas;
    }

    @Override
    public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        ViewHolder viewHolder = null;
        switch (viewType) {
            case TYPE_COMMON_VIEW:
                viewHolder = ViewHolder.create(mContext, getItemLayoutId(), parent);
                break;
        }
        return viewHolder;
    }

    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
        switch (holder.getItemViewType()) {
            case TYPE_COMMON_VIEW:
                bindCommonItem(holder, position);
                break;
        }
    }

    private void bindCommonItem(RecyclerView.ViewHolder holder, final int position) {
        final ViewHolder viewHolder = (ViewHolder) holder;
        convert(viewHolder, mDatas.get(position));
        viewHolder.getConvertView().setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                mItemClickListener.onItemClick(viewHolder, mDatas.get(position), position);
            }
        });
    }

    @Override
    public int getItemCount() {
        return mDatas.size();
    }

    @Override
    public int getItemViewType(int position) {
        return TYPE_COMMON_VIEW;
    }

    public T getItem(int position) {
        if (mDatas.isEmpty()) {
            return null;
        }
        return mDatas.get(position);
    }

    public void setOnItemClickListener(OnItemClickListeners<T> itemClickListener) {
        mItemClickListener = itemClickListener;
    }
}

很简单,继承RecyclerView.Adapter,重写相关方法,提供了getItemLayoutId()convert()两个抽象方法供BaseAdapter的子类实现,来初始化item的布局id,以及item内容,同时通过OnItemClickListeners接口为item绑定点击事件。

编写好了Adapter,我们在其构造方法中添加一个参数isOpenLoadMore,来表示是否开启加载更多:

public BaseAdapter(Context context, List<T> datas, boolean isOpenLoadMore) {
        mContext = context;
        mDatas = datas == null ? new ArrayList<T>() : datas;
        mOpenLoadMore = isOpenLoadMore;
    }

这样初级版本的Adapter就完成了。


6401.jpg

这个方法可以监听RecyclerView 的滚动。哈哈距离解决问题更近一步了。我们看看OnScrollListener 里面抽象方法的参数。

2、添加Footer View

接下来就要添加Footer View,这样才能有加载更多的视觉效果么。其实很简单,如果当前item的position满足如下条件:

private boolean isFooterView(int position) {
        return mOpenLoadMore && position >= getItemCount() - 1;
    }

即已经开启加载更多、当前position在列表的尾部,则在getItemViewType()返回

@Override
    public int getItemViewType(int position) {
        if (isFooterView(position)) {
            return TYPE_FOOTER_VIEW;
        }
    }

之后会创建Footer View对应的ViewHolder:

@Override
    public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        ViewHolder viewHolder = null;
        switch (viewType) {
            case TYPE_FOOTER_VIEW:
                if (mFooterLayout == null) {
                    mFooterLayout = new RelativeLayout(mContext);
                }
                viewHolder = ViewHolder.create(mFooterLayout);
                break;
        }
        return viewHolder;
    }

可以看到mFooterLayout是一个空的Container,因为要根据加载更多对应的状态来更新mFooterLayout,这个稍后再说。

这样Footer View就添加完了吗?当然没有,我们需要针对StaggeredGridLayoutManager、GridLayoutManager模式分别重写onViewAttachedToWindow()onAttachedToRecyclerView()方法,否则会出现Footer View不能在列表底部占据一行的问题:

@Override
    public void onViewAttachedToWindow(RecyclerView.ViewHolder holder) {
        super.onViewAttachedToWindow(holder);
        if (isFooterView(holder.getLayoutPosition())) {
            ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();

            if (lp != null && lp instanceof StaggeredGridLayoutManager.LayoutParams) {
                StaggeredGridLayoutManager.LayoutParams p = (StaggeredGridLayoutManager.LayoutParams) lp;
                p.setFullSpan(true);
            }
        }
    }

    @Override
    public void onAttachedToRecyclerView(RecyclerView recyclerView) {
        super.onAttachedToRecyclerView(recyclerView);
        final RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
        if (layoutManager instanceof GridLayoutManager) {
            final GridLayoutManager gridManager = ((GridLayoutManager) layoutManager);
            gridManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
                @Override
                public int getSpanSize(int position) {
                    if (isFooterView(position)) {
                        return gridManager.getSpanCount();
                    }
                    return 1;
                }
            });
        }
    }

到此无论是那种形式的列表都能正常添加Footer View了。

SwipeRefreshLayout

图片 2

Android 提供了 SwipeRefreshLayout 类实现下拉刷新的手势操作,在 BookListing 和 NewsApp 这两个应用中都使用了 SwipeRefreshLayout。例如下面的 XML 代码,应用的主要内容显示在 RecyclerView 中,要想实现它的下拉刷新功能,需要将 SwipeRefreshLayout 作为它的父视图 (Parent View),但是 SwipeRefreshLayout 只能有一个子视图,所以在 RecyclerView 之外还需要用 RelativeLayout 这个 ViewGroup 包括。另外,SwipeRefreshLayout 是由 Android 支持库提供的,所以使用前确保在项目的 Gradle 中添加了正确的依赖库。

<android.support.v4.widget.SwipeRefreshLayout 
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@ id/swipe_container"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <android.support.v7.widget.RecyclerView
            android:id="@ id/list"
            android:layout_width="match_parent"
            android:layout_height="wrap_content" />

        <TextView
            android:id="@ id/empty_view"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerInParent="true"
            android:gravity="center" />
    </RelativeLayout>
</android.support.v4.widget.SwipeRefreshLayout>

SwipeRefreshLayout 的 ID 设置为 swipe_container,用于在 Java 中查找这个 Android 组件,并设置监听器实现具体的刷新操作。例如下面的 Java 代码,在 onCreate 中设置 OnRefreshListener 监听器,并在其中 override onRefresh method,它会在用户完成下拉手势后调用,所以这里就是刷新应用内容需要执行的代码。另外,刷新动画的颜色序列可以在 setColorSchemeResources 中设置。

SwipeRefreshLayout swipeContainer = findViewById(R.id.swipe_container);

swipeContainer.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
    @Override
    public void onRefresh() {
        // ToDo: Handles the pull to refresh event here.
    }
});
// Configure the refreshing colors.
swipeContainer.setColorSchemeResources(
        android.R.color.holo_blue_light,
        android.R.color.holo_green_light,
        android.R.color.holo_orange_light,
        android.R.color.holo_red_light);

SwipeRefreshLayout 的刷新动画通常由用户的下拉手势触发,应用在完成刷新操作后停止刷新动画,通过设置以下 method 实现:

swipeContainer.setRefreshing(false);

如果设置 setRefreshingtrue 就可以主动开始刷新动画,所以 SwipeRefreshLayout 也可以用作加载指示符 (Loading Indicator),在加载数据的时候开始刷新动画,数据加载完成后停止刷新动画,在 BookListing 和 NewsApp 这两个应用中都是这么做的。

更多 SwipeRefreshLayout 内容可以参考这个 CodePath 教程。


视频列表滚动连播技术探究系列

1、仿网易/QQ空间视频列表滚动连播炫酷效果(V1.0 挖坑之路)。
2、仿网易/QQ空间视频列表滚动连播炫酷效果(V2.0 填坑之路) 想看源码的,看这篇文章。
3、仿网易视频列表滚动连播炫酷效果(v3.0 稳定版-思想改变及优化) 稳定版-进行优化和思想上的改变。
4、RecyclerView 平滑滚动可控制滚动速度的终极解决方案。
5、仿网易视频列表连播炫酷效果 - v3.1 升级版-细节优化(网络状态切换、item点击事件等)
持续更新中.....

voidonScrollStateChanged(RecyclerView recyclerView, int newState)Callback method to be invoked when RecyclerView's scroll state changes.voidonScrolled(RecyclerView recyclerView, int dx, int dy)Callback method to be invoked when the RecyclerView has been scrolled.

3、判断列表是否滚动到了底部

按照常理,只有滑动到列表的底部才会触发加载更多的操作,之前提到了onAttachedToRecyclerView()方法,通过该方法可以得到Adapter所绑定的RecyclerView,这样就能监听RecyclerView的滚动事件,进而判断列表是否滚动了底部:

private void startLoadMore(RecyclerView recyclerView, final RecyclerView.LayoutManager layoutManager) {
        if (!mOpenLoadMore || mLoadMoreListener == null) {
            return;
        }

        recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
            @Override
            public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
                super.onScrollStateChanged(recyclerView, newState);
                if (newState == RecyclerView.SCROLL_STATE_IDLE) {
                    if (!isAutoLoadMore && findLastVisibleItemPosition(layoutManager)   1 == getItemCount()) {
                        scrollLoadMore();
                    }
                }
            }

            @Override
            public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
                super.onScrolled(recyclerView, dx, dy);
                if (isAutoLoadMore && findLastVisibleItemPosition(layoutManager)   1 == getItemCount()) {
                    scrollLoadMore();
                } else if (isAutoLoadMore) {
                    isAutoLoadMore = false;
                }
            }
        });
    }

我们单独封装了startLoadMore()方法,当列表滚动状态改变会回调onScrollStateChanged()方法,如果状态为SCROLL_STATE_IDLE,并且当前可见的item位置为列表最后一项,则开始加载更多数据。这里还重写了onScrolled()方法,当列表滚动结束后会回调,重写该方法有什么用呢?如果初始item不满一屏幕,则可在该方法中加载更多数据,直到item占满一屏幕,也就自动加载更多。我们用isAutoLoadMore来区分这种情况,如果isAutoLoadMore为true,则Footer View可见则自动加载更多。

再看一下scrollLoadMore()方法:

private void scrollLoadMore() {
        if (mFooterLayout.getChildAt(0) == mLoadingView) {
            mLoadMoreListener.onLoadMore(false);
        }
    }

如果当前的Footer View 是正在加载的状态,则调用OnLoadMoreListener接口的onLoadMore()方法进行具体的加载操作,该方法有一个boolean类型的参数,表示是否重新加载,因为存在加载失败的情况,这样可方便使用。

Navigation Drawer

图片 3

Navigation Drawer 是 Android 应用中一种常用的导航模式,在 NewsApp 中用它来切换不同主题的新闻。使用 Android Studio 为应用添加 Navigation Drawer 非常简单,只需要在新建 Activity 时选择 Navigation Drawer Activity 就会自动创建好很多样板代码 (Boilerplate Code),样式符合 Material Design 风格,开发者仅需根据需求修改。以 NewsApp 为例:

In activity_main.xml

<android.support.v4.widget.DrawerLayout 
    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:id="@ id/drawer_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true"
    tools:openDrawer="start">

    <include
        layout="@layout/app_bar_main"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

    <android.support.design.widget.NavigationView
        android:id="@ id/nav_view"
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        android:layout_gravity="start"
        android:fitsSystemWindows="true"
        app:headerLayout="@layout/nav_header_main"
        app:menu="@menu/activity_main_drawer" />
</android.support.v4.widget.DrawerLayout>
  1. 以 DrawerLayout 作为根视图,显示应用内容的视图作为其子视图,与 NavigationView 互为兄弟视图。
  2. 显示应用内容的视图的宽高尺寸要设置为 match_parent,因为 Navigation Drawer 通常是隐藏的,不占用屏幕空间。
  3. NavigationView 必须是 DrawerLayout 的最后一个子视图,保证 Navigation Drawer 显示在屏幕的最顶层,这与 XML 的渲染次序有关。
  4. NavigationView 必须指定 android:layout_gravity 属性,即设置 Navigation Drawer 的呼出方向,通常是从左边滑出。这里设置为 start 而不是 left,是因为支持了从右至左 (RTL) 的设计语言,例如用户设备为 RTL 风格时,Navigation Drawer 是从右边滑出的。
  5. NavigationView 的高度设置为 match_parent,宽度设置为 wrap_content,实现抽屉的画面效果,而且通常宽度不会大于 320dp 以保证在抽屉打开时,部分应用内容仍可见。
  6. NavigationView 一般分为两部分布局:Header(通过 app:headerLayout 属性设置)和 Menu(通过 app:menu 属性设置)。注意两者的文件路径不同。
  7. 通过设置 tools:openDrawer 可以利用 DesignTime Layout Attributes 实时预览 Navigation Drawer 的显示效果。

设置好 Navigation Drawer 的布局后,接下来就在 Java 中初始化:

In MainActivity.java

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    Toolbar toolbar = findViewById(R.id.toolbar);
    setSupportActionBar(toolbar);

    DrawerLayout drawer = findViewById(R.id.drawer_layout);
    ActionBarDrawerToggle toggle = new ActionBarDrawerToggle(
        this, drawer, toolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close);
    drawer.addDrawerListener(toggle);
    toggle.syncState();

    NavigationView navigationView = findViewById(R.id.nav_view);
    navigationView.setCheckedItem(R.id.nav_overview);
    navigationView.setNavigationItemSelectedListener(this);

    ...
}
  1. 首先操作 ActionBarDrawerToggle 将 DrawerLayout 和 ActionBar 整合以提供 Navigation Drawer 的推荐设计风格,这是 Android Studio 自动生成的代码。
  2. 然后新建 NavigationView 对象并设置一个默认选中的子项 (item),item 的 ID 是在 NavigationView 的 Menu 资源中定义的。
  3. 最后将 NavigationItemSelectedListener 设置为 this 表示 MainActivity 是实现这个监听器接口的类。例如在 NewsApp 中,在 MainActivity 中 override onNavigationItemSelected method 处理 item 的选中事件。

In MainActivity.java

public class MainActivity extends AppCompatActivity
        implements NavigationView.OnNavigationItemSelectedListener {
    ...

    @Override
    public boolean onNavigationItemSelected(MenuItem item) {
        // Handle navigation view item clicks here.
        Toolbar toolbar = findViewById(R.id.toolbar);
        switch (item.getItemId()) {
            case R.id.nav_overview:
                toolbar.setTitle(R.string.app_name);
                section = null;
                break;
            case R.id.nav_news:
                toolbar.setTitle(R.string.menu_news);
                section = "news";
                break;
            case R.id.nav_opinion:
                toolbar.setTitle(R.string.menu_opinion);
                section = "commentisfree";
                break;
            default:
                Log.e(LOG_TAG, "Something wrong with navigation drawer items.");
        }

        // Close navigation drawer after handling item click event.
        DrawerLayout drawer = findViewById(R.id.drawer_layout);
        drawer.closeDrawer(GravityCompat.START);
        return true;
    }
  1. 由于 MainActivity 设置为实现 NavigationItemSelectedListener 接口的类,所以在类名后面需要添加 implements 参数。
  2. 用户通过选中不同的 item 时,通过 switch/case 语句进行相应的操作。
  3. 操作结束后,可以关闭 Navigation Drawer。注意这个操作由 DrawerLayout 完成,而不是 NavigationView。

除此之外,还需要修改 onBackPressed method 来指定当 Navigation Drawer 打开时,用户点击“返回”按钮 (Back buttons) 时的行为。

@Override
public void onBackPressed() {
    DrawerLayout drawer = findViewById(R.id.drawer_layout);
    if (drawer.isDrawerOpen(GravityCompat.START)) {
        drawer.closeDrawer(GravityCompat.START);
    } else {
        super.onBackPressed();
    }
}

当用户在Navigation Drawer 打开时点击“返回”按钮的操作应该是关闭 Navigation Drawer。这部分代码是由 Android Studio 自动生成的


技术前沿

万众瞩目Instant Apps终于全面问世啦 感兴趣的同学去看一下吧!

这没有办法获得RecyclerView 中最下面的元素是哪一个啊。要是每次滚动的时候,我们就能拿到RecyclerView所显示的Item中最下面的那个的位置就好了。不急我们看看LinearLayoutManager的文档

4、更新Footer View布局样式

到这里,我们已经明确了加载更多操作的触发时机,接下来就是在加载更多的时候来更新Footer View,我们定义了三种状态:加载中、加载失败、加载结束,通过如下方法将对应状态的View或布局id添加到Footer View中:

public void setLoadingView(int loadingId) {
        setLoadingView(Util.inflate(mContext, loadingId));
    }

public void setLoadFailedView(int loadFailedId) {
        setLoadFailedView(Util.inflate(mContext, loadFailedId));
    }

public void setLoadEndView(int loadEndId) {
        setLoadEndView(Util.inflate(mContext, loadEndId));
    }

这三个方法时是通过布局id来给Footer View设置新样式,当然还有通过View来设置的重载方法。在初始化Adapter时可以调用setLoadingView()来设置加载中的Footer View样式,如果加载失败了可调用setLoadFailedView()、如果加载结束没有更多数据则可以调用setLoadEndView()设对应的布局样式。其实就是先移除mFooterLayout的子View,然后将新的布局添加进去。

SearchView

图片 4

SearchView 是一种 Android 组件,相当于在应用栏放入一个 EditText,提供了很多搜索相关的功能,例如显示候选词等。在 BookListing App 中,使用 SearchView 来获取用户输入的搜索关键词,用于向 Web API 发送请求。

一、提供 menu 资源

<menu xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    <item
        android:id="@ id/menu_search"
        android:icon="@android:drawable/ic_menu_search"
        android:title="@string/search_title"
        app:actionViewClass="android.widget.SearchView"
        app:showAsAction="ifRoom|collapseActionView"
        android:orderInCategory="1" />
</menu>
  1. 通过 android:icon 属性设置 SearchView 出现在应用栏的图标。
  2. 通过 android:title 属性设置 SearchView 的标题。若未设置 SearchView 的图标时,就会在应用栏显示它的标题;用户长按图标时也会弹出标题文本消息。
  3. 通过 app:showAsAction 属性设置 SearchView 的显示策略,其中 ifRoom 表示SearchView 图标仅在应用栏有空间时才显示,否则会显示在溢出菜单 (Overflow Menu) 中;collapseActionView 表示 SearchView 会包含在一个二级菜单中。
  4. 通过 android:orderInCategory 属性设置 SearchView 的显示优先级,数字越小优先级越高。在应用栏有多个 item 时,如果它们的 app:showAsAction 属性都设置为 ifRoom,那么在应用栏没有空间时会按照这个属性仅显示优先级最高的菜单项。

二、在 Java 实现代码

与其它菜单项类似,SearchView 的操作也是在 onCreateOptionsMenu 中进行。

In MainActivity.java

@Override
public boolean onCreateOptionsMenu(Menu menu) {
    getMenuInflater().inflate(R.menu.options_menu, menu);

    searchMenuItem = menu.findItem(R.id.menu_search);
    searchView = (SearchView) searchMenuItem.getActionView();

    searchView.setQueryHint(getString(R.string.search_hint));
    searchView.setIconifiedByDefault(false);
    searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
        @Override
        public boolean onQueryTextSubmit(String query) {
            // Todo: Get the submitted query text here.
            return false;
        }

        @Override
        public boolean onQueryTextChange(String newText) {
            return false;
        }
    });
    return true;
}
  1. 调用 setQueryHint method 设置 SearchView 的提示文字。
  2. 调用 setIconifiedByDefault 设置 SearchView 是否默认显示图标,若真则仅显示图标,若假则显示带有文本输入框的完整 SearchView。在 BookListing App 中,由于在 menu 资源中设置了 app:showAsAction="collapseActionView" 将 SearchView 放入了二级菜单,所以在这里将 setIconifiedByDefault 设为 false 也仅显示 SearchView 的图标。
  3. 设置 SearchView 的 OnQueryTextListener 来获取用户输入的文本。其中必须 override 两个 method:onQueryTextSubmit 会在用户点击回车键后获取提交的文本;onQueryTextChange 则每当文本发生变化时就获取新的文本。

三、点击 TextView 自动打开 SearchView

在 BookListing App 中,提供了点击 Empty View 直接打开 SearchView,弹出输入法 (IME) 供用户输入搜索关键词的功能。

mEmptyStateView.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        searchMenuItem.expandActionView();
        searchView.setIconified(false);
    }
});
  1. 设置 Empty View 的 OnClickListener 并 override onClick method 添加打开 SearchView 的代码。
  2. 调用 MenuItem 的 expandActionView() 打开 SearchView 所在的应用栏二级菜单;再设置 SearchView 的 setIconifiedfalse 显示完整的 SearchView,系统就自动聚焦到 SearchView 的输入框,弹出输入法供用户输入搜索关键词了。

来个段子解解压

有个精神病到了银行,用手敲了敲柜台玻璃,问柜员:这是防弹玻璃吗?柜员:是的。精神病:能防得住炸弹吗?柜员吓得脸色苍白,说:不能!精神病从兜里掏出一对大小王贴在玻璃上说:“炸”!!!二十秒后,只见柜员怯怯的说出3个字。。。精神病满意的走了!

int findLastVisibleItemPosition ()Returns the adapter position of the last visible view. This position does not include adapter changes that were dispatched after the last layout pass.Note that, this value is not affected by layout orientation or item order traversal. (setReverseLayout Views are sorted by their positions in the adapter, not in the layout.If RecyclerView has item decorators, they will be considered in calculations as well.LayoutManager may pre-cache some views that are not necessarily visible. Those views are ignored in this method.

5、添加EmptyView

考虑一种情况,如果初始化时,需要先从网络请求数据,然后再更新列表,则一般需要有一个加载提示,所以我们有必要将这个小功能也封装到Adapter中,这样就省去了修改界面布局或者手动显示、隐藏加载提示的步骤。
实现也很简单,先看如下代码:

@Override
    public int getItemCount() {
        if (mDatas.isEmpty() && mEmptyView != null) {
            return 1;
        }
    }

如果mData为空,且设置了EmptyView则getItemCount()直接返回1。同理返回的item类型为TYPE_EMPTY_VIEW,代表EmptyView:

@Override
    public int getItemViewType(int position) {
        if (mDatas.isEmpty() && mEmptyView != null) {
            return TYPE_EMPTY_VIEW;
        }
    }

onCreateViewHolder()方法中会创建对应的ViewHolder。

@Override
    public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        ViewHolder viewHolder = null;
        switch (viewType) {
            case TYPE_EMPTY_VIEW:
                viewHolder = ViewHolder.create(mEmptyView);
                break;
        }
        return viewHolder;
    }

同时提供方法在初始化Adapter时设置EmptyView:

public void setEmptyView(View emptyView) {
        mEmptyView = emptyView;
    }
Endless Scrolling RecyclerView List

图片 5

在 RecyclerView 列表滑到底部之前,应用提前加载数据添加到列表中,实现无限滚动列表的效果。因此,这里要添加 OnScrollListener 并 override onScrolled method 来监控列表的滚动情况,当应用判断列表快要滑到底时,会加载更多数据。

recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
    @Override
    public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
        if (isLoading) {
            return;
        }

        if (dy > 0) {
            visibleItemCount = layoutManager.getChildCount();
            totalItemCount = layoutManager.getItemCount();
            pastVisibleItems = layoutManager.findFirstVisibleItemPosition();

            if ((visibleItemCount   pastVisibleItems) >= totalItemCount) {
            isLoading = true;
            // Todo: Fetch new data here.
            }
        }
    }
});
  1. onScrolled 中,首先判断 isLoading 是否为真,若是则提前返回。isLoading 是一个全局的布尔类型变量,默认为 false。它表示当前状态下数据是否正在加载中,所以在开始加载数据时需要将它设置为 true,数据加载完成时设为 false。
  2. 利用 onScrolled 的参数 dy 大于零(表示屏幕的滚动方向为向上)时分别获取三个参数。由于这三个变量是在匿名类中使用的,所以要声明为全局变量。
    (1)visibleItemCount:获取 RecyclerView 的 item 数目,但不包括已回收的视图,所以它可以看作是当前屏幕可见的 item 数目。
    (2)totalItemCount:获取 RecyclerView 的所有 item 数目。
    (3)pastVisibleItems:获取 RecyclerView.Adapter 第一个可见的 item 的位置,也就是当前屏幕可见的第一个 item 的位置,所以它可以看作是已滑出屏幕的 item 数目。
  3. 根据上述三个参数判断列表滑到底时,设置 isLoading 为 true,并添加加载更多数据的代码。在 NewsApp 中的做法是设置新的 URL 请求参数后重启 AsyncTaskLoader 加载数据,并在数据加载完成后判断此次加载是否用了新的请求参数,若是则将数据添加到列表中,实现无限滚动列表的效果。

仿网易视频列表滚动连播炫酷效果

先看效果图,再说实现思想

图片 6

图片 7

gif不清晰,主要看实现的功能

这个方法就是找到最后一个可见Item的位置。太好了,只要每次滚动的时候监听一下最后一个可见的Item 的位置,我们就可以决定是否架子啊更多了。感觉我们已经准备的差不多了。那我们开始吧

6、具体使用

完成了封装,来看看具体的使用,首先创建一个RefreshAdapter继承我们的BaseAdapter:

public class RefreshAdapter extends BaseAdapter<String> {

    public RefreshAdapter(Context context, List<String> datas, boolean isLoadMore) {
        super(context, datas, isLoadMore);
    }

    @Override
    protected void convert(ViewHolder holder, final String data) {
        holder.setText(R.id.item_title, data);
        holder.setOnClickListener(R.id.item_btn, new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Toast.makeText(mContext, "我是"   data   "的button", Toast.LENGTH_SHORT).show();
            }
        });
    }

    @Override
    protected int getItemLayoutId() {
        return R.layout.item_layout;
    }
}

getItemLayoutId()中返回item布局id,在convert()中初始化item的内容。有了RefreshAdapter,接下来看Activity的操作:

@Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mRecyclerView = (RecyclerView) findViewById(R.id.recyclerview);

        //初始化adapter
        mAdapter = new RefreshAdapter(this, null, true);

        //初始化EmptyView
        View emptyView = LayoutInflater.from(this).inflate(R.layout.empty_layout, (ViewGroup) mRecyclerView.getParent(), false);
        mAdapter.setEmptyView(emptyView);

        //初始化 开始加载更多的loading View
        mAdapter.setLoadingView(R.layout.load_loading_layout);

        //设置加载更多触发的事件监听
        mAdapter.setOnLoadMoreListener(new OnLoadMoreListener() {
            @Override
            public void onLoadMore(boolean isReload) {
                loadMore();
            }
        });

        //设置item点击事件监听
        mAdapter.setOnItemClickListener(new OnItemClickListeners<String>() {

            @Override
            public void onItemClick(ViewHolder viewHolder, String data, int position) {
                Toast.makeText(MainActivity.this, data, Toast.LENGTH_SHORT).show();
            }
        });

        LinearLayoutManager layoutManager = new LinearLayoutManager(this);
        layoutManager.setOrientation(LinearLayoutManager.VERTICAL);
        mRecyclerView.setLayoutManager(layoutManager);

        mRecyclerView.setAdapter(mAdapter);


        //延时3s刷新列表
        new Handler().postDelayed(new Runnable() {
            @Override
            public void run() {
                List<String> data = new ArrayList<>();
                for (int i = 0; i < 12; i  ) {
                    data.add("item--"   i);
                }
                //刷新数据
                mAdapter.setNewData(data);
            }
        }, 3000);
    }

注释已经很详细了,就不多说了。其中loadMore()方法如下:

private void loadMore() {

        new Handler().postDelayed(new Runnable() {
            @Override
            public void run() {

                if (mAdapter.getItemCount() > 15 && isFailed) {
                    isFailed = false;
                    //加载失败,更新footer view提示
                    mAdapter.setLoadFailedView(R.layout.load_failed_layout);
                } else if (mAdapter.getItemCount() > 17) {
                    //加载完成,更新footer view提示
                    mAdapter.setLoadEndView(R.layout.load_end_layout);
                } else {
                    final List<String> data = new ArrayList<>();
                    for (int i = 0; i < 2; i  ) {
                        data.add("item--"   (mAdapter.getItemCount()   i - 1));
                    }
                    //刷新数据
                    mAdapter.setLoadMoreData(data);
                }
            }
        }, 2000);
    }

就是延时2s更新列表数据,同时人为模拟加载失败和结束的情况。

RecyclerView clear & addAll

由于 RecyclerView 没有提供与类似 ListView 的 clear 和 addAll method,所以需要开发者自行实现,通常是在 RecyclerView.Adapter 中添加辅助方法 (Helper Method)。

In NewsAdapter.java

public void clear() {
    mBooksList.clear();
    notifyDataSetChanged();
}

public void addAll(List<News> newsList) {
    mBooksList.addAll(newsList);
    notifyDataSetChanged();
}

上面两个辅助方法都调用了同一个 method,告知适配器列表数据有变化。列表数据变化通常有两种类型:一种是子项变化 (Item Change),指 item 的数据变化,列表没有任何位置上的变化;另一种是结构变化 (Structural Change),指列表中有 item 插入、移除、移动。常见的 notify 类 method 有以下几种:

Method Description
notifyDataSetChanged() 未指定数据变化的类型,适配器认为所有的原先数据已不可用,LayoutManager 会重新捆绑 (rebind) 和重新布局 (relayout) 视图,这种方式效率较低,通常不优先考虑使用。
notifyItemChanged (int position) 列表中指定位置 (position) 的 item 发生数据变化,这属于子项变化,适配器仅更新该位置的 item,其它 item 不受影响。
notifyItemInserted (int position) 列表中在指定位置 (position) 插入 item,原先该位置的 item 往后移一位 (position 1),其它 item 仅改变位置,不会重新布局。这属于结构变化。
notifyItemMoved (int fromPosition, int toPosition) 列表中一个 item 从原先位置 (fromPosition) 移动到另一位置 (toPosition),其它 item 仅改变位置,不会重新布局。这属于结构变化。
notifyItemRemoved (int position) 列表中指定位置 (position) 的 item 被移除,该位置后面的 item 位置前移一位 (position - 1),其它 item 仅改变位置,不会重新布局。这属于结构变化。
notifyItemRangeChanged (int positionStart, int itemCount) 从指定位置 (positionStart) 开始,共计 itemCount 个数的 item 发生数据变化,这属于子项变化,适配器仅更新相应的 item,其它 item 不受影响。

根据不同的情景使用不同的 notify 类 method 以达到更高效率,更多信息可以到 RecyclerView.Adapter文档查看。


实现思路

首先分析功能:1、滚动时不播放,但是要亮起,当前屏幕内,item view显示百分比最大的一个。
2、停止滚动且手指抬起时自动播放。
3、播放完当前的视频,自动滚动到下一个并自动播放。
4、正在播放的当前视频,快要播放完毕时,弹出TextView提示播放下一个,点击TextView自动滚动到下一个。
以上就是我们要实现的功能点。

代码设计

根据上面的思路,我们设计一下代码。1.当需要加载更多的时候,肯定需要回调啊。那好我们先定义一个回调的接口:

 interface OnLoadingMore { void onLoadMore();}

2.如果正在加载的时候,用户又上下的滑动,再触发了加载更多,而上次的触发的加载还没有结束,怎么办?定义一个变量 ,如果正在加载了,还没有完成,不能再次加载。

private boolean isLoading;

3.为了提高用户的体验,我们应该设置一个用户滑到提前多少个Item的时候触发加载更多的变量。

private int visibleThreshold = 5;

4.RecyclerView 应该有两种类型的(暂且这么分吧,实际开发中可能会超过)。

@Override public int getItemViewType(int position) { if (position == getItemCount { return TYPE_LOAD_MORE; } else { return TYPE_NORMAL; } }

感觉已经准备的差不多了,可以开始了

我们选择在BaseAdapter 中实现自动加载更多的功能,可以方便子类使用。贴代码啦:

public class BaseAdapter<T> extends RecyclerView.Adapter<BaseViewHolder> { List<T> dataSet = new ArrayList<>(); private final int TYPE_LOAD_MORE = 100; private final int TYPE_NORMAL = 101; private boolean isLoading; private int visibleThreshold = 5; OnLoadingMore loadingMore;private boolean canLoadMore = true; public BaseAdapter(RecyclerView recyclerView) { //传入一个RecyclerView 下面就是监听滚动啦 recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { @Override public void onScrolled(RecyclerView recyclerView, int dx, int dy) { super.onScrolled(recyclerView, dx, dy); LinearLayoutManager layoutManager = (LinearLayoutManager) recyclerView.getLayoutManager(); int itemCount = layoutManager.getItemCount(); int lastPosition = layoutManager.findLastVisibleItemPosition(); Log.i("lastPosition --> ", lastPosition   ""); Log.i("itemCount --> ", itemCount   " "); //如果当前不是正在加载更多,并且到了该加载更多的位置,加载更多。 if (!isLoading && (lastPosition >= (itemCount - visibleThreshold))) { if (canLoadMore&&loadingMore != null) { isLoading = true; loadingMore.onLoadMore(); } } } }); } @Override public BaseViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { View view; if (viewType == TYPE_LOAD_MORE) { view = LayoutInflater.from(parent.getContext.inflate(R.layout.item_load_more, parent, false); ProgressBar progressBar = (ProgressBar) view.findViewById(R.id.pb_loading); progressBar.setInterpolator(new AccelerateInterpolator; progressBar.setIndeterminate; } else { view = LayoutInflater.from(parent.getContext.inflate(R.layout.item_normal, parent, false); } return new BaseViewHolder<>; } @Override public void onBindViewHolder(BaseViewHolder holder, int position) { if (getItemViewType == TYPE_LOAD_MORE) { View itemView=holder.itemView; //判定是不是可以加载更多或则正在加载 if (canLoadMore && isLoading) { if (itemView.getVisibility() != View.VISIBLE) { itemView.setVisibility(View.VISIBLE); } } else if (itemView.getVisibility() == View.VISIBLE) { itemView.setVisibility(View.GONE); } } else { TextView textView =  holder.itemView; textView.setText(String.valueOf); } } @Override public int getItemCount() { return dataSet.size()   1; } //分成两种类型 1.普通的item 2.底部的Progress Bar @Override public int getItemViewType(int position) { if (position == getItemCount { return TYPE_LOAD_MORE; } else { return TYPE_NORMAL; } } public void setLoadingMore(OnLoadingMore loadingMore) { this.loadingMore = loadingMore; } public void setLoading(boolean loading) { isLoading = loading; } public void addData { dataSet.add; notifyDataSetChanged(); }public void setCanLoadMore(boolean canLoadMore) { this.canLoadMore = canLoadMore;} //回调接口 interface OnLoadingMore { void onLoadMore(); }}

对应的布局文件:1.item_load_more: 很简单的一个ProgressBar 加上 一个TextView

<?xml version="1.0" encoding="utf-8"?><RelativeLayout xmlns:andro android:layout_width="match_parent" android:layout_height="wrap_content"> <ProgressBar android:layout_width="20dp" android:layout_height="20dp" android: android:layout_toLeftOf="@ id/tv_msg" /> <TextView android: android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerInParent="true" android:text="正在加载" /></RelativeLayout>

2.item_normal:只是一个TextView

<?xml version="1.0" encoding="utf-8"?><TextView xmlns:andro android:layout_width="match_parent" android:layout_height="wrap_content" android:paddingTop="10dp" android:paddingBottom="10dp" android:textColor="#000"></TextView>

效果截图:

图片 8截图图片 9截图

哈哈还是挺简单的吧。有问题的可以回复我。

完整的代码请移步github

7、效果

运行后,看具体的效果:

图片 10

EmptyView

图片 11

loading

图片 12

load_failed

图片 13

load_end

图片 14

auto_load

在 RecyclerView 子项间添加分隔线

图片 15

DividerItemDecoration 属于 RecyclerView.ItemDecoration 的子类,它可用于为 LinearLayoutManager 下的 item 添加分隔线,支持垂直和水平方向。

LinearLayoutManager layoutManager = new LinearLayoutManager(this);
recyclerView.setLayoutManager(layoutManager);

DividerItemDecoration dividerItemDecoration = new DividerItemDecoration(
        recyclerView.getContext(), layoutManager.getOrientation());
recyclerView.addItemDecoration(dividerItemDecoration);
  1. DividerItemDecoration 提供了很多 method 可以为分隔线提供更多设置,例如 setDrawable 可以为分隔线设置 Drawable 资源。
  2. 如果 RecyclerView 不采用 LinearLayoutManager,那么可以使用 RecyclerView.ItemDecoration 来进行更精细的分隔线设置。

分析功能点

  1. 滚动时不播放,但是要亮起,当前屏幕内,item view显示百分比最大的一个

分析: 看到滚动两个字,首要想起的是滚动监听(这里使用RecyclerView) addOnScrollListener,我们要在列表滚动时,计算屏幕内item 的数量,且找出item view 显示的百分比,进行比较,若其中一个item 百分比为:100 或百分比占最大,我们亮起这个item,其他的item是暗色的。

ok,顺着这个思路,往下走,如何获取一个view的百分比呢?
这是我从网上搜到的,这样我们就可以获取一个view他在当前屏幕内所占的百分比了。

 private final Rect mCurrentViewRect = new Rect();
 public int getVisibilityPercents(View view) {

        int percents = 100;

        view.getLocalVisibleRect(mCurrentViewRect);

        int height = view.getHeight();

        if (viewIsPartiallyHiddenTop()) {
            // view is partially hidden behind the top edge
            percents = (height - mCurrentViewRect.top) * 100 / height;
        } else if (viewIsPartiallyHiddenBottom(height)) {
            percents = mCurrentViewRect.bottom * 100 / height;
        }

        return percents;
    }

    private boolean viewIsPartiallyHiddenBottom(int height) {
        return mCurrentViewRect.bottom > 0 && mCurrentViewRect.bottom < height;
    }

    private boolean viewIsPartiallyHiddenTop() {
        return mCurrentViewRect.top > 0;
    }

既然拿到了百分比,就可以在recyclerview 的滚动监听中进行计算了,看下面代码

 rl_video.addOnScrollListener(new RecyclerView.OnScrollListener() {
            @Override
            public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
                super.onScrollStateChanged(recyclerView, newState);
                switch (newState) {
                    case RecyclerView.SCROLL_STATE_IDLE://停止滑动
                        mScrollState = false;
                        //滑动停止和松开手指时,调用此方法 进行播放
                        aoutPlayVideo(recyclerView);
                        break;
                    case RecyclerView.SCROLL_STATE_DRAGGING://用户用手指滚动
                        mScrollState = true;
                        break;
                    case RecyclerView.SCROLL_STATE_SETTLING://自动滚动
                        mScrollState = true;
                        break;
                }
            }

            @Override
            public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
                super.onScrolled(recyclerView, dx, dy);
                RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
                if (layoutManager instanceof LinearLayoutManager) {
                    LinearLayoutManager linearManager = (LinearLayoutManager) layoutManager;
                    //获取最后一个可见view的位置
                    lastItemPosition = linearManager.findLastVisibleItemPosition();
                    //获取第一个可见view的位置
                    firstItemPosition = linearManager.findFirstVisibleItemPosition();
                    //获取可见view的总数
                    visibleItemCount = linearManager.getChildCount();
                    if (mScrollState) { //滚动
                        srcollVisible(recyclerView, firstItemPosition, lastItemPosition, visibleItemCount);
                    } else { //停止 第一次进入时调用此方法,进行播放
                        aoutPlayVideo(recyclerView);
                    }
                }
            }
        });

很简单的逻辑,同理我们的功能2 也就能实现了

  1. 停止滚动且手指抬起时自动播放。
    什么时候回调这个状态呢 case RecyclerView.SCROLL_STATE_IDLE,可以打个log 测试下,当列表停止滑动且手指抬起时,就会调这个状态。那我们就可以在这个状态里,实现播放视频的逻辑了。

  2. 如何计算百分比,实现思路非常简单,简单的for循环而已,具体的都写有注释。

   /**
     * 滚动时 判断哪个view 显示百分比最大,哪个最大 视图亮起
     *
     * @param recyclerView
     * @param firstItemPosition
     * @param lastItemPosition
     * @param visibleItemCount  屏幕显示的item数量
     */
    private void srcollVisible(RecyclerView recyclerView, int firstItemPosition, int lastItemPosition, int visibleItemCount) {

        for (int i = 0; i < visibleItemCount; i  ) {
            if (recyclerView != null) {
                View childAt = recyclerView.getChildAt(i).findViewById(R.id.visiabile);
                recyclerView.getChildAt(i).findViewById(R.id.video_masked).setVisibility(View.VISIBLE);
                int visibilityPercents = VisibilePercentsUtils.getInstance().getVisibilityPercents(childAt);
                if (visibilityPercents == 100) {
                    position = i;
                }
            }
        }

        itemPosition = (firstItemPosition   position);
        recyclerView.getChildAt(position).findViewById(R.id.video_masked).setVisibility(View.GONE);
        Log.e("linksu MainActivity",
                "srcollVisible(MainActivity.java:94) itemPosition --> "   itemPosition   " playerPosition:"   playerPosition);
        if (playerPosition == itemPosition) {// 说明还是之前的 item 并没有滑动到下一个
            Log.e("linksu MainActivity",
                    "srcollVisible(MainActivity.java:109) 还是当前的item 没有变化 继续播放");
        } else { // 说明亮起的已经不是当前的item了,是下一个或者之前的那个,我们停止变暗的item的播放
            Log.e("linksu MainActivity",
                    "srcollVisible(MainActivity.java:120) stopPlayer:"   playerPosition);
            VideoHolder childViewHolder = (VideoHolder) recyclerView.findViewHolderForAdapterPosition(playerPosition);
            if (childViewHolder != null) {
                childViewHolder.stopPlayer();
                childViewHolder.unRegisterVideoPlayerListener();// 注意我们需要解除上一个item的监听,不然会注册多个监听
            }
            playerPosition = itemPosition;
        }
    }
  1. 播放完当前的视频,自动滚动到下一个并自动播放。
    思路:监听视频播放状态(这里用的是七牛开源的播放器)。
public interface OnVideoPlayerListener {
    void onVideoPrepared();

    void onVideoCompletion();

    void onVideoError(int i,String error);

    void onBufferingUpdate();

    void onVideoStopped();

    void onVideoPlayingPro(long currentPosition, long mDuration, int mPlayStatus);
}

然后再ViewHolder 中实现监听,并在ViewHolder 中写一个接口 ,Activity 中实现这个接口

    public interface onHolderVideoPlayerListener {
        void videoError();

        void videoCompletion();

        void videoBuffer();

        void videoTips();

        void missVideoTips();
    }

    private onHolderVideoPlayerListener listener;

    /**
     * 注册监听
     *
     * @param listener
     */
    public void registerVideoPlayerListener(onHolderVideoPlayerListener listener) {
        this.listener = listener;
    }

    /**
     * 解除监听
     */
    public void unRegisterVideoPlayerListener() {
        if (listener != null) {
            listener = null;
        }
    }

需要注意的是我们要为 item 注册 和解除监听,否则item 移除屏幕后,还会继续监听,这样就乱套了。
同理,每个item的播放器也是及时的解除监听状态,否则我们拿到的播放进度会有问题。

ViewHolder 实现 OnVideoPlayerListener,

@Override
    public void onVideoPrepared() {

    }

    @Override
    public void onVideoCompletion() {
        if (listener != null) {
            img.setVisibility(View.VISIBLE);
            video_masked.setVisibility(View.VISIBLE);
            listener.videoCompletion();
            lVideoView.unOnVideoPlayerListener();// 注意 注销监听 否则之前的item 都会有监听
        }
    }

    @Override
    public void onVideoError(int i, String error) {
        if (listener != null) {
            listener.videoError();
        }
    }

    @Override
    public void onBufferingUpdate() {

    }

    @Override
    public void onVideoStopped() {
        img.setVisibility(View.VISIBLE);
        video_masked.setVisibility(View.VISIBLE);
        lVideoView.unOnVideoPlayerListener();// 注意 注销监听 否则之前的item 都会有监听
    }

    @Override
    public void onVideoPlayingPro(long currentPosition, long mDuration, int mPlayStatus) {
        float percent = (float) ((double) currentPosition / (double) mDuration);
        DecimalFormat fnum = new DecimalFormat("##0.0");
        float c_percent = 0;
        c_percent = Float.parseFloat(fnum.format(percent));
        if (0.8 <= c_percent) {
            if (listener != null) {
                listener.videoTips();
            }
        } else {
            if (listener != null) {
                listener.missVideoTips();
            }
        }
    }

Activity 的实现思路,看下面代码

implements VideoHolder.onHolderVideoPlayerListener

  @Override
    public void videoError() {

    }

    @Override
    public void videoCompletion() { //播放完成 播放下一个
        int p = (itemPosition   1);
        rl_video.smoothScrollToPosition(p);
    }

    @Override
    public void videoBuffer() {

    }

    @Override
    public void videoTips() {
        mTv.setVisibility(View.VISIBLE);
    }

    @Override
    public void missVideoTips() {
        mTv.setVisibility(View.GONE);
    }

需要注意的是 当我们调用 rl_video.smoothScrollToPosition(p); recyclerview的滚动监听会回调,我们需要在这样滚动的时候这样处理

同理 ,下面这段代码也处理了,我们用手拖动时,若当前item 百分比最大 视频继续播放。

itemPosition = (firstItemPosition   position);
        recyclerView.getChildAt(position).findViewById(R.id.video_masked).setVisibility(View.GONE);
        Log.e("linksu MainActivity",
                "srcollVisible(MainActivity.java:94) itemPosition --> "   itemPosition   " playerPosition:"   playerPosition);
        if (playerPosition == itemPosition) {// 说明还是之前的 item 并没有滑动到下一个
            Log.e("linksu MainActivity",
                    "srcollVisible(MainActivity.java:109) 还是当前的item 没有变化 继续播放");
        } else { // 说明亮起的已经不是当前的item了,是下一个或者之前的那个,我们停止变暗的item的播放
            Log.e("linksu MainActivity",
                    "srcollVisible(MainActivity.java:120) stopPlayer:"   playerPosition);
            VideoHolder childViewHolder = (VideoHolder) recyclerView.findViewHolderForAdapterPosition(playerPosition);
            if (childViewHolder != null) {
                childViewHolder.stopPlayer();
                childViewHolder.unRegisterVideoPlayerListener();// 注意我们需要解除上一个item的监听,不然会注册多个监听
            }
            playerPosition = itemPosition;
        }
  1. 正在播放的当前视频,快要播放完毕时,弹出TextView提示播放下一个,点击TextView
    思路:这个就很简单了,上面的代码也有给出。我说一下思路,已知我们在ViewHolder 中监听视频播放状态同时也监听了播放进度,如下代码:
 @Override
    public void onVideoPlayingPro(long currentPosition, long mDuration, int mPlayStatus) {
        float percent = (float) ((double) currentPosition / (double) mDuration);
        DecimalFormat fnum = new DecimalFormat("##0.0");
        float c_percent = 0;
        c_percent = Float.parseFloat(fnum.format(percent));
        if (0.8 <= c_percent) {
            if (listener != null) {
                listener.videoTips();
            }
        } else {
            if (listener != null) {
                listener.missVideoTips();
            }
        }
    }

已知我们在Activity 中实现了监听 onHolderVideoPlayerListener ,这样就很好去处理了

   @Override
    public void videoTips() {
        mTv.setVisibility(View.VISIBLE);
    }

    @Override
    public void missVideoTips() {
        mTv.setVisibility(View.GONE);
    }

    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.tv:
                int p = (itemPosition   1);
                rl_video.smoothScrollToPosition(p);
                break;
        }
    }

这样我们就实现了所有的功能点,这里视频播放器我用的是七牛开源的播放器,具体的封装就不写出来了,都是写简单的监听以及回调。下面我给出主要的代码,供大家参考。
VideoHolder

package com.linksu.mydemo;

import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;

import com.linksu.video_manager_library.listener.OnVideoPlayerListener;
import com.linksu.video_manager_library.ui.LVideoView;

import java.text.DecimalFormat;
import java.util.List;

/**
 * ================================================
 * 作    者:linksus
 * 版    本:1.0
 * 创建日期:7/27 0027
 * 描    述:
 * 修订历史:
 * ================================================
 */
public class VideoHolder extends RecyclerView.ViewHolder implements OnVideoPlayerListener {
    View video_masked;
    TextView item_tv;
    List<String> mlist;
    ImageView img;
    LVideoView lVideoView;

    public VideoHolder(View itemView, List<String> mlist) {
        super(itemView);
        this.item_tv = (TextView) itemView.findViewById(R.id.item_tv);
        this.lVideoView = (LVideoView) itemView.findViewById(R.id.lvideoview);
        this.mlist = mlist;
        this.img = (ImageView) itemView.findViewById(R.id.img);
        this.video_masked = itemView.findViewById(R.id.video_masked);
    }

    public void update(int position) {
        String s = mlist.get(position);
        item_tv.setText(s);
    }

    public void player(int position) {
        String url = mlist.get(position);
        if (lVideoView != null) {
            lVideoView.setOnVideoPlayerListener(this);
            lVideoView.startLive(url);
        }
    }

    public void stopPlayer() {
        if (lVideoView != null) {
            lVideoView.stopVideoPlay();
        }
    }

    public void goneMasked() {
        img.setVisibility(View.GONE);
        video_masked.setVisibility(View.GONE);
    }

    @Override
    public void onVideoPrepared() {

    }

    @Override
    public void onVideoCompletion() {
        if (listener != null) {
            img.setVisibility(View.VISIBLE);
            video_masked.setVisibility(View.VISIBLE);
            listener.videoCompletion();
            lVideoView.unOnVideoPlayerListener();// 注意 注销监听 否则之前的item 都会有监听
        }
    }

    @Override
    public void onVideoError(int i, String error) {
        if (listener != null) {
            listener.videoError();
        }
    }

    @Override
    public void onBufferingUpdate() {

    }

    @Override
    public void onVideoStopped() {
        img.setVisibility(View.VISIBLE);
        video_masked.setVisibility(View.VISIBLE);
        lVideoView.unOnVideoPlayerListener();// 注意 注销监听 否则之前的item 都会有监听
    }

    @Override
    public void onVideoPlayingPro(long currentPosition, long mDuration, int mPlayStatus) {
        float percent = (float) ((double) currentPosition / (double) mDuration);
        DecimalFormat fnum = new DecimalFormat("##0.0");
        float c_percent = 0;
        c_percent = Float.parseFloat(fnum.format(percent));
        if (0.8 <= c_percent) {
            if (listener != null) {
                listener.videoTips();
            }
        } else {
            if (listener != null) {
                listener.missVideoTips();
            }
        }
    }

    public interface onHolderVideoPlayerListener {
        void videoError();

        void videoCompletion();

        void videoBuffer();

        void videoTips();

        void missVideoTips();
    }

    private onHolderVideoPlayerListener listener;

    /**
     * 注册监听
     *
     * @param listener
     */
    public void registerVideoPlayerListener(onHolderVideoPlayerListener listener) {
        this.listener = listener;
    }

    /**
     * 解除监听
     */
    public void unRegisterVideoPlayerListener() {
        if (listener != null) {
            listener = null;
        }
    }
}

VideoAdapter

package com.linksu.mydemo;

import android.content.Context;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

import java.util.List;

/**
 * ================================================
 * 作    者:linksus
 * 版    本:1.0
 * 创建日期:7/27 0027
 * 描    述:
 * 修订历史:
 * ================================================
 */
public class VideoAdapter extends RecyclerView.Adapter<VideoHolder> {
    private List<String> mlist;
    private Context context;

    public VideoAdapter(List<String> mlist, Context context) {
        this.mlist = mlist;
        this.context = context;
    }

    @Override
    public VideoHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View inflater = LayoutInflater.from(context).inflate(R.layout.item, parent, false);
        return new VideoHolder(inflater, mlist);
    }

    @Override
    public void onBindViewHolder(VideoHolder holder, int position) {
        holder.update(position);
    }

    @Override
    public int getItemCount() {
        return mlist.size();
    }

    @Override
    public long getItemId(int position) {
        return position;
    }
}

MainActivity

package com.linksu.mydemo;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.view.View;
import android.widget.TextView;
import android.widget.Toast;

import java.util.ArrayList;
import java.util.List;

public class MainActivity extends AppCompatActivity implements VideoHolder.onHolderVideoPlayerListener, View.OnClickListener {

    private RecyclerView rl_video;
    private LinearLayoutManager layoutManager;
    private VideoAdapter adapter;
    private List<String> mList = new ArrayList<>();
    private boolean mScrollState = false;//是否处于滚动状态
    private int lastItemPosition;
    private int firstItemPosition;
    private int visibleItemCount;
    private TextView mTv;

    private int maxPercents = 0; // 最大显示百分比
    private int position = 0; // 最大显示百分比的屏幕内的子view的位置
    private int itemPosition = 0;// item 的位置
    private int playerPosition = 0;//正在播放item 的位置

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        for (int i = 0; i < 30; i  ) {
            mList.add("http://rmrbtest-image.peopleapp.com/upload/video/201707/149999980163e892f63bf5cb85.mp4");
            mList.add("http://rmrbtest-image.peopleapp.com/upload/video/201707/1499914158feea8c512f348b4a.mp4");
            mList.add("http://rmrbtest-image.peopleapp.com/upload/video/201707/14991545431a9b3f9b6dd22db2.mp4");
            mList.add("http://rmrbtest-image.peopleapp.com/upload/video/201707/14991537461a9b3f9b6dd22db2.mp4");
            mList.add("http://rmrbtest-image.peopleapp.com/upload/video/201706/14963009301c0fd671d0e3ae1b.mp4");
            mList.add("http://rmrbtest-image.peopleapp.com/upload/video/201705/14962013161a9b3f9b6dd22db2.mp4");
            mList.add("http://rmrbtest-image.peopleapp.com/upload/video/201705/14958540601a9b3f9b6dd22db2.mp4");
            mList.add("http://rmrbtest-image.peopleapp.com/upload/video/201705/14957861291a9b3f9b6dd22db2.mp4");
        }
        rl_video = (RecyclerView) findViewById(R.id.rl_video);
        mTv = (TextView) findViewById(R.id.tv);
        mTv.setOnClickListener(this);
        layoutManager = new LinearLayoutManager(this);
        rl_video.setLayoutManager(layoutManager);
        adapter = new VideoAdapter(mList, this);
        rl_video.setAdapter(adapter);
        rl_video.addOnScrollListener(new RecyclerView.OnScrollListener() {
            @Override
            public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
                super.onScrollStateChanged(recyclerView, newState);
                switch (newState) {
                    case RecyclerView.SCROLL_STATE_IDLE://停止滑动
                        mScrollState = false;
                        //滑动停止和松开手指时,调用此方法 进行播放
                        aoutPlayVideo(recyclerView);
                        break;
                    case RecyclerView.SCROLL_STATE_DRAGGING://用户用手指滚动
                        mScrollState = true;
                        break;
                    case RecyclerView.SCROLL_STATE_SETTLING://自动滚动
                        mScrollState = true;
                        break;
                }
            }

            @Override
            public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
                super.onScrolled(recyclerView, dx, dy);
                RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
                if (layoutManager instanceof LinearLayoutManager) {
                    LinearLayoutManager linearManager = (LinearLayoutManager) layoutManager;
                    //获取最后一个可见view的位置
                    lastItemPosition = linearManager.findLastVisibleItemPosition();
                    //获取第一个可见view的位置
                    firstItemPosition = linearManager.findFirstVisibleItemPosition();
                    //获取可见view的总数
                    visibleItemCount = linearManager.getChildCount();
                    if (mScrollState) { //滚动
                        srcollVisible(recyclerView, firstItemPosition, lastItemPosition, visibleItemCount);
                    } else { //停止 第一次进入时调用此方法,进行播放
                        aoutPlayVideo(recyclerView);
                    }
                }
            }
        });
    }

    /**
     * 滚动时 判断哪个view 显示百分比最大,哪个最大 视图亮起
     *
     * @param recyclerView
     * @param firstItemPosition
     * @param lastItemPosition
     * @param visibleItemCount  屏幕显示的item数量
     */
    private void srcollVisible(RecyclerView recyclerView, int firstItemPosition, int lastItemPosition, int visibleItemCount) {

        for (int i = 0; i < visibleItemCount; i  ) {
            if (recyclerView != null) {
                View childAt = recyclerView.getChildAt(i).findViewById(R.id.visiabile);
                recyclerView.getChildAt(i).findViewById(R.id.video_masked).setVisibility(View.VISIBLE);
                int visibilityPercents = VisibilePercentsUtils.getInstance().getVisibilityPercents(childAt);
                if (visibilityPercents == 100) {
                    position = i;
                }
            }
        }

        itemPosition = (firstItemPosition   position);
        recyclerView.getChildAt(position).findViewById(R.id.video_masked).setVisibility(View.GONE);
        Log.e("linksu MainActivity",
                "srcollVisible(MainActivity.java:94) itemPosition --> "   itemPosition   " playerPosition:"   playerPosition);
        if (playerPosition == itemPosition) {// 说明还是之前的 item 并没有滑动到下一个
            Log.e("linksu MainActivity",
                    "srcollVisible(MainActivity.java:109) 还是当前的item 没有变化 继续播放");
        } else { // 说明亮起的已经不是当前的item了,是下一个或者之前的那个,我们停止变暗的item的播放
            Log.e("linksu MainActivity",
                    "srcollVisible(MainActivity.java:120) stopPlayer:"   playerPosition);
            VideoHolder childViewHolder = (VideoHolder) recyclerView.findViewHolderForAdapterPosition(playerPosition);
            if (childViewHolder != null) {
                childViewHolder.stopPlayer();
                childViewHolder.unRegisterVideoPlayerListener();// 注意我们需要解除上一个item的监听,不然会注册多个监听
            }
            playerPosition = itemPosition;
        }
    }

    /**
     * 1.停止滚动手指抬起时 开始播放视频
     *
     * @param recyclerView
     */
    private void aoutPlayVideo(final RecyclerView recyclerView) {
        Log.e("linksu MainActivity",
                "aoutPlayVideo(MainActivity.java:112) position --> "   position);
        VideoHolder childViewHolder = (VideoHolder) recyclerView.findViewHolderForAdapterPosition(itemPosition);
        if (childViewHolder != null) {
            childViewHolder.registerVideoPlayerListener(this);
            childViewHolder.goneMasked();
            childViewHolder.player(itemPosition);
        }
    }

    @Override
    public void videoError() {

    }

    @Override
    public void videoCompletion() { //播放完成 播放下一个
        int p = (itemPosition   1);
        rl_video.smoothScrollToPosition(p);
    }

    @Override
    public void videoBuffer() {

    }

    @Override
    public void videoTips() {
        mTv.setVisibility(View.VISIBLE);
    }

    @Override
    public void missVideoTips() {
        mTv.setVisibility(View.GONE);
    }

    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.tv:
                int p = (itemPosition   1);
                rl_video.smoothScrollToPosition(p);
                break;
        }
    }
}

PS:更新

(1)重构基类继承关系
(2)支持多种类型的Item View


创建只有一种类型的Item View的Adapter时,直接继承CommonBaseAdapter类即可,其它操作不变。

创建有多种类型的Item View的Adapter时时,继承MultiBaseAdapter即可,实例如下:

public class MultiRefreshAdapter extends MultiBaseAdapter<String> {

    public MultiRefreshAdapter(Context context, List<String> datas, boolean isOpenLoadMore) {
        super(context, datas, isOpenLoadMore);
    }

    @Override
    protected void convert(ViewHolder holder, final String data, int viewType) {
        if (viewType == 0) {
            holder.setText(R.id.item_title, data);
            holder.setOnClickListener(R.id.item_btn, new View.OnClickListener() {
                @Override
                public void onClick(View view) {
                    Toast.makeText(mContext, "我是"   data   "的button", Toast.LENGTH_SHORT).show();
                }
            });
        } else {
            holder.setText(R.id.item_title1, data);
        }
    }

    @Override
    protected int getItemLayoutId(int viewType) {
        if (viewType == 0) {
            return R.layout.item_layout;
        }
        return R.layout.item_layout1;
    }

    @Override
    protected int getViewType(int position, String data) {
        if (position % 2 == 0) {
            return 0;
        }
        return 1;
    }
}

设置Item点击事件时,通过如下方法:

mAdapter.setOnMultiItemClickListener(new OnMultiItemClickListeners<String>() {
            @Override
            public void onItemClick(ViewHolder viewHolder, String data, int position, int viewType) {

            }
        });

其它的操作不变。效果就不贴了,可通过源码查看。

Expandable CardView

图片 16

在 BookListing App 中,RecyclerView 使用了 CardView 作为其子项的主要布局,并且实现了可扩展的 CardView 效果。实现这一功能有三个要点。

一、OnItemClickListener

RecyclerView 没有类似 ListView 可直接调用的类来处理 item 的点击事件,RecyclerView 只提供了 OnItemClickListener 接口,所以首先需要在 RecyclerView.Adapter 中实现 OnItemClickListener,以 BookListing App 为例,代码如下。

In BookAdapter.java

private OnItemClickListener mOnItemClickListener;

public void setOnItemClickListener(OnItemClickListener OnItemClickListener) {
    mOnItemClickListener = OnItemClickListener;
}

public interface OnItemClickListener {
    void onItemClick(View view, int position);
}

然后在 Mainactivity 中设置监听器,代码如下。

In MainActivity.java

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    ...

    mAdapter.setOnItemClickListener(new BookAdapter.OnItemClickListener() {
        @Override
        public void onItemClick(View view, int position) {
        }
    });

    ...
}

针对 BookListing App 的情况,CardView 的点击事件不需要在 MainActivity 中进行任何操作,所以这里留空,但必须在 MainActivity 中设置监听器。所有操作放在监听器内进行,因此又回到 RecyclerView.Adapter 中去。

In BookAdapter.java

@Override
public void onBindViewHolder(final MyViewHolder holder, final int position) {
    ...

    if (mOnItemClickListener != null) {
        holder.cardView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                cardViewIndex = holder.getAdapterPosition();
                notifyItemChanged(holder.getAdapterPosition());
            }
        });
    }

    ...
}

onBindViewHolder 中设置监听器并通过 override onClick method 添加 CardView 点击事件触发后执行的代码。由于 BookListing App 要实现 CardView 的展开和折叠功能,所以在这里使用了一个全局变量记录当前用户点击的 CardView 的位置,并通过 notifyItemChanged 告知监听器更新该位置的 item 数据。注意 cardViewIndex 是全局变量,默认值为 -1,使其默认情况下无作用 (unreachable),直到发生点击事件时对它赋值。

二、展开和折叠 CardView

接下来适配器会更新发生点击事件的 item 数据,也就是重新执行一次 onBindViewHolder,position 参数为 cardViewIndex。所以,此时就要往 onBindViewHolder 添加扩展 CardView 的代码了。

@Override
public void onBindViewHolder(final MyViewHolder holder, final int position) {
    ...

    if (cardViewIndex == position) {
        ViewGroup.LayoutParams cardViewLayoutParams = holder.cardView.getLayoutParams();

        if (isCardExpanded.get(position).equals(false)) {
            cardViewLayoutParams.height = (int) mContext.getResources().
                    getDimension(R.dimen.card_expanded_height);

            int expandedHorizontalMargin = mContext.getResources().
                    getDimensionPixelOffset(R.dimen.card_expanded_horizontal_margin);
            int expandedVerticalMargin = mContext.getResources().
                    getDimensionPixelOffset(R.dimen.card_expanded_vertical_margin);
            setMargins(holder.cardView, expandedHorizontalMargin, expandedVerticalMargin,
                    expandedHorizontalMargin, expandedVerticalMargin);

            isCardExpanded.set(position, true);
        } else {
             cardViewLayoutParams.height = (int) mContext.getResources().
                    getDimension(R.dimen.card_height);

            int originVerticalMargin = mContext.getResources().
                    getDimensionPixelOffset(R.dimen.card_vertical_margin);
            int originHorizontalMargin = mContext.getResources().
                    getDimensionPixelOffset(R.dimen.card_horizontal_margin);
            setMargins(holder.cardView, originHorizontalMargin, originVerticalMargin,
                    originHorizontalMargin, originVerticalMargin);

            isCardExpanded.set(position, false);
        }

        holder.cardView.setLayoutParams(cardViewLayoutParams);

        cardViewIndex = -1;
    }

    ...
}
  1. 首先通过 if/else 语句保证监听器只更新发生点击事件的 item,并在更新完毕后将 cardViewIndex 重新设为 -1,使其默认情况下无作用。
  2. 为了精简篇幅,以上代码仅以 CardView 的操作举例,删去了显示副标题、作者、简介、链接的 TextView 以及显示图片的 ImageView 在 CardView 展开和折叠情况下的操作逻辑。完整代码请参考我的 GitHub BookListing Repository。
  3. 通过设置 ViewGroup.LayoutParams 的 height 参数改变 CardView 的高度,达到展开和折叠的效果。
  4. 通过辅助方法 setMargins 改变 CardView 与屏幕边缘的距离,达到放大和缩小的效果。其中 setMargins 的输入参数为像素值 (px),可利用 mContext.getResources().getDimensionPixelOffset() 实现独立像素 (dp) 对像素 (px) 的转换。
/**
 * Helper method that set margins of views, using {@link ViewGroup.MarginLayoutParams}.
 *
 * @param view         is the view whom set margins to.
 * @param leftMargin   is the left margin of the view.
 * @param topMargin    is the top margin of the view.
 * @param rightMargin  is the right margin of the view.
 * @param bottomMargin is the bottom margin of the view.
 */
private void setMargins(View view, int leftMargin, int topMargin,
                        int rightMargin, int bottomMargin) {
    if (view.getLayoutParams() instanceof ViewGroup.MarginLayoutParams) {
        ViewGroup.MarginLayoutParams params =
                (ViewGroup.MarginLayoutParams) view.getLayoutParams();
        params.setMargins(leftMargin, topMargin, rightMargin, bottomMargin);
        view.requestLayout();
    }
}
  1. CardView 在展开和折叠过程中的动画效果是由 DefaultItemAnimator 提供的,在 MainActivity 中添加以下指令即可。

     recyclerView.setItemAnimator(new DefaultItemAnimator());
    
  2. 设置好需要修改的 LayoutParams 参数后,最后不要忘记执行以下指令,使修改设置生效。

     holder.cardView.setLayoutParams(cardViewLayoutParams);
    
  3. 大家肯定注意到,与 CardView 展开和折叠相关的参数不止有 cardViewIndex,还有一个全局布尔类型变量 isCardExpanded,它实际上是一个 ArrayList<Boolean>,记录了 RecyclerView 列表的每个 item 的展开和折叠情况,CardView 展开时为真,折叠时为假。因此,在展开某个位置的 CardView 后需要将该位置的 isCardExpanded 设为 true,折叠后则设为 false。如何获取一个与 RecyclerView 列表等长的 ArrayList<Boolean> 并将所有项默认为 false(因为 CardView 默认是折叠的)就是第三个要点。

三、isCardExpanded

由于 RecyclerView.Adapter 必须 override getItemCount method,在这个 method 中会得到 RecyclerView 列表的所有 item 数目,因此可以在 getItemCount 内初始化 isCardExpanded,代码如下。

private List<Boolean> isCardExpanded = new ArrayList<>();

@Override
public int getItemCount() {
    int listItemCount = mBooksList.size();
    if (isCardExpanded.size() < listItemCount) {
        isCardExpanded.clear();

        for (int index = 0; index < listItemCount; index  ) {
            isCardExpanded.add(false);
        }
    }
    return listItemCount;
}
  1. isCardExpanded 的数据类型定义为 List<Boolean>,仅在定义对象实例时指定为 ArrayList<Boolean>,这是因为 List 是接口,而 ArrayList 是 List 的具象类,当 App 需要重构代码 (refactor) 时,例如由 ArrayList 改为 LinkedList,仅在对象实例的定义处指定一个具象类即可,保持代码的灵活性。
  2. getItemCount 内,首先判断当前 isCardExpanded 是否已有值,若无才对其赋值,并在赋值之前清除列表,最后通过 for 循环语句向 isCardExpanded 添加与 RecyclerView 列表等长的 item 并将所有项默认为 false。
  3. 事实上,对于 BookListing App 来说,RecyclerView 列表的 item 数目一直都是 10,但是这里没有将 isCardExpanded 硬编码为长度为 10 的 ArrayList,保持良好的编程习惯。

以上就是主要实现的代码以及思路,纯手工,从思考到实现,不管实现什么功能思路最重要了,只要有了思路就可以用代码写出来,有不理解的地方可以留言给我。

推荐 好用的翻墙工具 ,主要是稳定。
仿网易/QQ空间视频列表滚动连播炫酷效果(V2.0 填坑之路) 想看源码的,看这篇文章。

图片 17

专题封面

2016.12.6更新

使用EmptyView时,初始加载无数据可移除EmptyView,或添加新ReloadView以便进行重新加载、提示等操作。

先显示文字,后显示图片

图片 18

在 BookListing App 中,列表中的每一本图书都包含标题、作者、评分等文字,还有一张图片。因为应用的内容是通过 AsyncTaskLoader 从 Web API 获取的,文字与图片的数据大小量级不同,为了尽快为用户提供有意义的内容,所以 BookListing App 采取了“先显示文字,后显示图片”的策略,这就要求图书的文字和图片分开两个线程加载,用到两个 AsyncTaskLoader。
以 BookListing App 为例,在 MainActivity 中引入两个 AsyncTaskLoader,它们的 LoaderCallback 作为一个类定义,在操作 Loader 时传入的参数也需要更改。

In MainActivity.java

public class MainActivity extends AppCompatActivity {
    ...

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ...

        getLoaderManager().initLoader(BOOK_LOADER_ID, null, new BookLoaderCallback());
    }

    private class BookLoaderCallback implements LoaderManager.LoaderCallbacks<List<Book>> {
        @Override
        public Loader<List<Book>> onCreateLoader(int i, Bundle bundle) {
            ...
        }

        @Override
        public void onLoadFinished(Loader<List<Book>> loader, List<Book> books) {
            ...

            loaderManager.restartLoader(IMAGE_LOADER_ID, null, new ImageLoaderCallback());
        }
    }

    private class ImageLoaderCallback implements LoaderManager.LoaderCallbacks<List<Drawable>> {
        @Override
        public Loader<List<Drawable>> onCreateLoader(int i, Bundle bundle) {
            return new ImageLoader(getApplicationContext());
        }

        @Override
        public void onLoadFinished(Loader<List<Drawable>> loader, List<Drawable> drawables) {
            mAdapter.setImage(drawables);
        }
    }
}
  1. 在 NewsApp 中,因为只用到了一个 AsyncTaskLoader,所以直接把 MainActivity 作为它的 LoaderCallback 类,在 MainActivity 类名后面添加 implements 参数。而在 BookListing App 中就需要在 MainActivity 内分别定义两个 BookLoaderCallback 和 ImageLoaderCallback 类,并在类名后面添加 implements 参数。在调用 initLoaderrestartLoader 时第三个参数也要由 this 改为各自的 LoaderCallback 类实例,如 new BookLoaderCallback()new ImageLoaderCallback()
  2. BookListing App 采用“先显示文字,后显示图片”的策略,所以在加载完文字后再开始加载图片,也就是说,在 BookLoaderCallback 的 onLoadFinished 执行 restartLoader 指令,开启 ImageLoader。
  3. 在 ImageLoaderCallback 的 onCreateLoader 中,ImageLoader 直接跳到后台线程 loadInBackground 将 Web API 返回的图片 URL (QueryUtils.image) 转换为 Drawable 资源。返回值的数据类型为 List<Drawable>。

In ImageLoader.java

@Override
public List<Drawable> loadInBackground() {
    List<Drawable> drawables = new ArrayList<>();

    List<String> image = QueryUtils.image;

    if (image != null && !image.isEmpty()) {
        for (int index = 0; index < image.size(); index  ) {
            drawables.add(getImageDrawable(image.get(index)));
        }
    }

    return drawables;
}

这里用到了辅助方法 getImageDrawable,涉及到显示网络图片的内容,主要是应用了 InputStream 缓存并转换为 Drawable 资源,返回值的数据类型为 Drawable。

private static Drawable getImageDrawable(String imageUrlString) {
    Drawable imageResource = null;

    try {
        URL url = new URL(imageUrlString);
        InputStream content = (InputStream) url.getContent();
        imageResource = Drawable.createFromStream(content, "src");
    } catch (MalformedURLException e) {
        Log.e(LOG_TAG, "Problem building the URL ", e);
    } catch (IOException e) {
        Log.e(LOG_TAG, "Problem getting the URL content ", e);
    }

    return imageResource;
}
  1. ImageLoader 完成图片数据加载后,在 ImageLoaderCallback 的 onLoadFinished 中调用 RecyclerView.Adapter 的 setImage 辅助方法,向列表中添加图片。
public void setImage(List<Drawable> drawables) {
    if (drawables != null && !drawables.isEmpty()) {
        for (int index = 0; index < drawables.size(); index  ) {
            mBooksList.get(index).setImageResource(drawables.get(index));
            notifyItemChanged(index);
        }
    }
}

通过 for 循环语句为 RecyclerView 列表的每一项添加图片,并通知适配器每一项的数据变化,使其得以更新。


2017.7.4更新

支持Adapter重置、完善使用方式

NestedScrollView

图片 19

在 BookListing App 中,除 RecyclerView 之外还有其它视图需要随着 RecyclerView 的列表一起实现滚动效果,例如图书列表上面的两个分别显示图书总数和页码信息的 TextView,所以这里引入 NestedScrollView。

<android.support.v4.widget.NestedScrollView
    android:id="@ id/scroll_view"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:fadeScrollbars="true"
    android:scrollbars="vertical">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content">

            <TextView
                android:id="@ id/result_count"
                style="@style/resultTextView"
                android:gravity="start|center_vertical" />

            <TextView
                android:id="@ id/result_page"
                style="@style/resultTextView"
                android:gravity="end|center_vertical" />
        </LinearLayout>

        <android.support.v7.widget.RecyclerView
            android:id="@ id/list"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:clipToPadding="false"
            android:paddingBottom="@dimen/recycler_view_bottom_padding" />
    </LinearLayout>
</android.support.v4.widget.NestedScrollView>
  1. NestedScrollView 与 ScrollView 类似,只能有一个子视图。针对 RecyclerView 和 ListView 垂直方向的滚动,NestedScrollView 提供了更灵活的滚动效果,而且无需任何 Java 代码默认开启滚动效果。
  2. 在 NestedScrollView 中 设置 android:scrollbars 属性为 vertical 使其拥有一个垂直方向的滚动条,默认在右侧显示;同时设置 android:fadeScrollbarstrue 使滚动条在列表静止不动时隐藏。这两个属性并不是 NestedScrollView 专有的,事实上它是在 View 类定义的,所以理论上所有视图都可以设置这两个属性。
  3. RecyclerView 设置了 android:paddingBottom 使列表的最后一个 item 距离屏幕底部有一定的距离,但是这会导致内容滚动时在 padding 区域出现一个空白横条,非常影响美观。所以这里还需要设置 android:clipToPaddingfalse 使 padding 的空白区域在内容滚动时消失,仅在列表滚动到底部时出现。

将 RecyclerView 放在 NestedScrollView 内可能会出现 RecyclerView 列表滚动卡顿不流畅的现象,根据 stack overflow 的高票答案来看,在 Java 中添加以下代码即可解决问题。

recyclerView.setNestedScrollingEnabled(false);

不过在 stack overflow 的答案下面也有评论指出,执行这条代码后 RecyclerView 将不会回收视图,导致资源浪费。由于这条指令在 RecyclerView 文档中没有详细介绍,我通过 Android Profiler 也没有观察到异常,所以就没有深究下去,有了解的各位请不吝赐教。


2017.12.22更新

  1. 支持给RecyclerView添加HeaderView
  2. 自动判断是否正在加载更多,避免重复加载

更多详情可参考源码,不合理的地方还求反馈!
☞源码戳这里

Empty View

图片 20

BookListing 和 NewsApp 这两个应用的数据都是从 Web API 获取的,所以在设备无网络连接或无数据的情况下,要用 Empty View 显示当前应用的状态,提醒用户进行下一步操作。

在 XML 布局中,通常把 RecyclerView 与 Empty View 放入 RelativeLayout 中,彼此不用设置相对位置关系,因为两者在同一时间只会显示其一。

<RelativeLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <android.support.v7.widget.RecyclerView
        android:id="@ id/list"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

    <TextView
        android:id="@ id/empty_view"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:gravity="center"
        android:padding="@dimen/activity_spacing" />
</RelativeLayout>

设置 Empty View 需要在多处实现,将重复使用的代码封装成一个 Helper method 供其它地方调用是一个好的做法。

private void setEmptyView(boolean visibility, @Nullable Integer textStringId,
                          @Nullable Integer imageDrawableId) {
    TextView emptyView = findViewById(R.id.empty_view);
    if (visibility && textStringId != null && imageDrawableId != null) {
        emptyView.setText(textStringId);
        emptyView.setCompoundDrawablesWithIntrinsicBounds(null,
                ContextCompat.getDrawable(getApplicationContext(), imageDrawableId),
                null, null);
        emptyView.setCompoundDrawablePadding(getResources().
                getDimensionPixelOffset(R.dimen.compound_image_spacing));
        emptyView.setVisibility(View.VISIBLE);
    } else {
        emptyView.setVisibility(View.GONE);
    }
}
  1. setEmptyView 设置了三个输入参数,第一个是设置 Empty View 是否可见的布尔类型参数;第二个是 Empty View 的文本字符串 ID,可以为 null;第三个是 Empty View 的图片资源 ID,可以为 null。注意设置为 @Nullable 的输入参数不能是原始数据类型,所以这里需要将 int 换成其对象类型 Integer。

  2. 如果要设置 Empty View 为不可见,可以调用以下代码。

     setEmptyView(false, null, null);
    
  3. 仅当依次传入 true、字符串 ID、以及图片资源 ID 后,Empty View 才会开始设置相应的属性,最后设置为可见。其中,设置 TextView 的组合图片 (Compound Drawable) 需要调用 setCompoundDrawablesWithIntrinsicBounds 并通过 ContextCompat.getDrawable() 获取 Drawable 资源传入第二个参数,表示在 TextView 上方显示一张图片。

  4. 调用 setCompoundDrawablePadding 设置图片与文本之间的间隔,它传入的参数是像素值 (px),可以通过 getResources().getDimensionPixelOffset() 实现独立像素 (dp) 对像素 (px) 的转换。


onSaveInstanceState

在面对设备旋转等会导致 Activity 重启的情况时,可以将一些变量在 Activity 被杀死 (killed) 之前保存起来,然后 Activity 重启时在 onCreate 或 onRestoreInstanceState 中取回变量。例如在 BookListing App 中,通过 override onSaveInstanceState method 保存了 resultOffset 整数以及 requestKeywords 字符串。

@Override
public void onSaveInstanceState(Bundle savedInstanceState) {
    savedInstanceState.putInt("resultOffset", resultOffset);
    savedInstanceState.putString("requestKeywords", requestKeywords);

    super.onSaveInstanceState(savedInstanceState);
}
  1. 参数是以字符串键/值的形式存在的,在取回变量时也是根据字符串键作为每个变量的 ID 来识别的。
  2. 最后不要忘了调用 onSaveInstanceState 的超级类。

变量可以在 onCreate 中取回,例如在 BookListing App 中,当 savedInstanceState 不为空时,按字符串键取回 resultOffset 整数以及 requestKeywords 字符串。注意在 onCreate 的输入参数就是 savedInstanceState。

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    if (savedInstanceState != null) {
        resultOffset = savedInstanceState.getInt("resultOffset");
        requestKeywords = savedInstanceState.getString("requestKeywords");
    }

    ...
}

变量也可以在 onRestoreInstanceState 中取回,只不过它是在 onCreate 之后执行的,因此如果变量是需要在 onCreate 中用到的,就不能在 onRestoreInstanceState 中取回变量了。

@Override
public void onRestoreInstanceState(Bundle savedInstanceState) {
    super.onRestoreInstanceState(savedInstanceState);

    resultOffset = savedInstanceState.getInt("resultOffset");
    requestKeywords = savedInstanceState.getString("requestKeywords");
}

注意 onSaveInstanceState 和 onRestoreInstanceState 调用各自的超级类的时机是不一样的。


横滑手势捕捉

图片 21

在 BookListing App 中,采用了底部横滑切换上下页的导航模式,实现方法主要参考了这个 stack overflow 帖子,主要是应用了 OnTouchListener 中的 SimpleOnGestureListener 来捕捉左滑和右滑手势操作。
不过在 BookListing App 中的应用不够理想,例如局部的横滑通常是面向局部操作的,例如移除屏幕中的一个卡片。另外设置了 OnTouchListener 的视图会让 Android Studio 认为该视图是一个自定义视图,提示无障碍 (Accessibility) 方面的警告。因此,这部分内容仅作为备忘,不作讨论。


检查网络状态

在 BookListing 和 NewsApp 这两个应用中,在进行数据加载之前都需要检查网络状态。面对这种经常用到的功能,封装成一个 Helper method 供其它地方调用是一个好的做法。

private boolean isConnected() {
    // Get a reference to the ConnectivityManager to check state of network connectivity.
    ConnectivityManager connMgr = (ConnectivityManager)
            getSystemService(Context.CONNECTIVITY_SERVICE);
    // Get details on the currently active default data network.
    NetworkInfo networkInfo = connMgr.getActiveNetworkInfo();

    // Return true if the device is connected, vice versa.
    return networkInfo != null && networkInfo.isConnected();
}

该辅助方法返回的数据类型是布尔类型,当检查到设备已连接网络时返回值为真,无连接时为假。这样一来 isConnected() 就可以轻易地放入 if/else 流控语句应用。


格式化 ISO-8601 时间

在 NewsApp 中,使用的 The Guardian API 返回的时间数据是 ISO-8601 格式的,具体来说是 UTC 日期与时间结合 (Combined date and time in UTC) 的形式。这种格式会在时间前面加一个大写字母 T,显示 UTC 时间时在末尾加一个大写字母 Z。这只是复杂的时间问题的冰山一角,大家有兴趣可以观看这个 YouTube 视频。所幸在 Android 中可以使用 SimpleDateFormat 来格式化时间,例如格式化 ISO-8601 时间可以利用如下代码:

try {
    SimpleDateFormat inFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.getDefault());
    Date dateIn = inFormat.parse(news.getTime());

    SimpleDateFormat outFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault());
    String dateOut = outFormat.format(dateIn);
} catch (ParseException e) {
    Log.e(LOG_TAG, "Problem parsing the news time.", e);
}
  1. 首先通过 SimpleDateFormat 指定输入的时间格式,然后在 try/catch 区块中解析 (parse) 输入的时间,获得一个 Date 对象;
  2. 最后通过 SimpleDateFormat 指定输出的时间格式,并将上面获得的 Date 对象传入 format method,获得预期格式的时间字符串。

触摸反馈

图片 22

之前的课程中提到,为视图提供触摸反馈,最简单的方法是设置视图的背景:

android:background="?android:attr/selectableItemBackground"

它实际上是应用了 R.attr 类提供的 Drawable 资源,在视图聚焦或点击 (focus/pressed) 状态下显示圆形涟漪的动画触摸反馈。常用的还有另外一个资源。

android:background="?android:attr/selectableItemBackgroundBorderless"

由于它是从 API Level 21 引入的,所以对于 minSdkVersion 在 API Level 21 以下的应用可以在 styles 中分开定义,在 BookListing App 中就是这么做的。它可以忽略视图的边界,在聚焦或点击 (focus/pressed) 时显示完整的圆形涟漪动画。这在一些不想由于显示视图边界而破坏界面完整性的场景很有帮助。


设置字体

图片 23

字体 属于 Android 应用的一类资源,它可以像图片、音频等资源一样引用。例如在 NewsApp 中,新闻标题的首字母采用了 Hansa Gotisch 字体(来源:Font Meme),实现方法是在 res/font 目录下存放 TTF 文件,然后在 TextView 中设置 android:fontFamily 属性为对应的 TTF 文件名即可。


实战项目 7&8 BookListing 和 NewsApp 这两个应用的分享完毕,欢迎大家到我的 GitHub 交流,文中有遗漏的要点也可以提醒我,我很乐意解答。

本文由新葡京8455发布,转载请注明来源

关键词: