标签: RecyclerView

Android—RecyclerView实现横向滑动翻页

*近项目需求要实现RecyclerView的分页滑动 先上效果图如下(视频压缩成的gif所以滑动切换效果有点卡顿了 效果为每页三条数据的滑动)

效果图

%title插图%num

一:创建横向布局管理器
/**
* Created by Sunny on 2019/4/1.
*/
public class HorizontalPageLayoutManager extends RecyclerView.LayoutManager implements PageDecorationLastJudge {
@Override
public RecyclerView.LayoutParams generateDefaultLayoutParams() {
return null;
}

int totalHeight = 0;
int totalWidth = 0;
int offsetY = 0;
int offsetX = 0;

public HorizontalPageLayoutManager(int rows, int columns) {
this.rows = rows;
this.columns = columns;
this.onePageSize = rows * columns;
}

@Override
public boolean canScrollHorizontally() {
return true;
}

@Override
public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, RecyclerView.State state) {
detachAndScrapAttachedViews(recycler);
int newX = offsetX + dx;
int result = dx;
if (newX > totalWidth) {
result = totalWidth – offsetX;
} else if (newX < 0) {
result = 0 – offsetX;
}
offsetX += result;
offsetChildrenHorizontal(-result);
recycleAndFillItems(recycler, state);
return result;
}

private SparseArray<Rect> allItemFrames = new SparseArray<>();

private int getUsableWidth() {
return getWidth() – getPaddingLeft() – getPaddingRight();
}

private int getUsableHeight() {
return getHeight() – getPaddingTop() – getPaddingBottom();
}

int rows = 0;
int columns = 0;
int pageSize = 0;
int itemWidth = 0;
int itemHeight = 0;
int onePageSize = 0;
int itemWidthUsed;
int itemHeightUsed;

@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {

if (getItemCount() == 0) {
removeAndRecycleAllViews(recycler);
return;
}
if (state.isPreLayout()) {
return;
}
//获取每个Item的平均宽高
itemWidth = getUsableWidth() / columns;
itemHeight = getUsableHeight() / rows;

//计算宽高已经使用的量,主要用于后期测量
itemWidthUsed = (columns – 1) * itemWidth;
itemHeightUsed = (rows – 1) * itemHeight;

//计算总的页数

// pageSize = state.getItemCount() / onePageSize + (state.getItemCount() % onePageSize == 0 ? 0 : 1);
computePageSize(state);
Log.i(“zzz”, “itemCount=” + getItemCount() + ” state itemCount=” + state.getItemCount() + ” pageSize=” + pageSize);
//计算可以横向滚动的*大值
totalWidth = (pageSize – 1) * getWidth();

//分离view
detachAndScrapAttachedViews(recycler);

int count = getItemCount();
for (int p = 0; p < pageSize; p++) {
for (int r = 0; r < rows; r++) {
for (int c = 0; c < columns; c++) {
int index = p * onePageSize + r * columns + c;
if (index == count) {
//跳出多重循环
c = columns;
r = rows;
p = pageSize;
break;
}

View view = recycler.getViewForPosition(index);
addView(view);
//测量item
measureChildWithMargins(view, itemWidthUsed, itemHeightUsed);

int width = getDecoratedMeasuredWidth(view);
int height = getDecoratedMeasuredHeight(view);
//记录显示范围
Rect rect = allItemFrames.get(index);
if (rect == null) {
rect = new Rect();
}
int x = p * getUsableWidth() + c * itemWidth;
int y = r * itemHeight;
rect.set(x, y, width + x, height + y);
allItemFrames.put(index, rect);

}
}
//每一页循环以后就回收一页的View用于下一页的使用
removeAndRecycleAllViews(recycler);
}

recycleAndFillItems(recycler, state);
}

private void computePageSize(RecyclerView.State state) {
pageSize = state.getItemCount() / onePageSize + (state.getItemCount() % onePageSize == 0 ? 0 : 1);
}

@Override
public void onDetachedFromWindow(RecyclerView view, RecyclerView.Recycler recycler) {
super.onDetachedFromWindow(view, recycler);
offsetX = 0;
offsetY = 0;
}

private void recycleAndFillItems(RecyclerView.Recycler recycler, RecyclerView.State state) {
if (state.isPreLayout()) {
return;
}

Rect displayRect = new Rect(getPaddingLeft() + offsetX, getPaddingTop(), getWidth() – getPaddingLeft() – getPaddingRight() + offsetX, getHeight() – getPaddingTop() – getPaddingBottom());
Rect childRect = new Rect();
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
childRect.left = getDecoratedLeft(child);
childRect.top = getDecoratedTop(child);
childRect.right = getDecoratedRight(child);
childRect.bottom = getDecoratedBottom(child);
if (!Rect.intersects(displayRect, childRect)) {
removeAndRecycleView(child, recycler);
}
}

for (int i = 0; i < getItemCount(); i++) {
if (Rect.intersects(displayRect, allItemFrames.get(i))) {
View view = recycler.getViewForPosition(i);
addView(view);
measureChildWithMargins(view, itemWidthUsed, itemHeightUsed);
Rect rect = allItemFrames.get(i);
layoutDecorated(view, rect.left – offsetX, rect.top, rect.right – offsetX, rect.bottom);
}
}

}

@Override
public boolean isLastRow(int index) {
if (index >= 0 && index < getItemCount()) {
int indexOfPage = index % onePageSize;
indexOfPage++;
if (indexOfPage > (rows – 1) * columns && indexOfPage <= onePageSize) {
return true;
}
}

return false;
}

@Override
public boolean isLastColumn(int position) {
if (position >= 0 && position < getItemCount()) {
position++;
if (position % columns == 0) {
return true;
}
}
return false;
}

@Override
public boolean isPageLast(int position) {
position++;
return position % onePageSize == 0;
}

@Override
public int computeHorizontalScrollRange(RecyclerView.State state) {
computePageSize(state);
return pageSize * getWidth();
}

@Override
public int computeHorizontalScrollOffset(RecyclerView.State state) {
return offsetX;
}

@Override
public int computeHorizontalScrollExtent(RecyclerView.State state) {
return getWidth();
}
}
二:创建接口PageDecorationLastJudge实现接口回调
public interface PageDecorationLastJudge {
/**
* Is the last row in one page
*
* @param position
* @return
*/
boolean isLastRow(int position);

/**
* Is the last Colum in one row;
*
* @param position
* @return
*/
boolean isLastColumn(int position);

boolean isPageLast(int position);
}
三:实现RecyclerView横向滚动工具类
/**
* 实现RecycleView分页滚动的工具类
* Created by Sunny on 2019/4/1.
*/

public class PagingScrollHelper {

RecyclerView mRecyclerView = null;

private MyOnScrollListener mOnScrollListener = new MyOnScrollListener();

private MyOnFlingListener mOnFlingListener = new MyOnFlingListener();
private int offsetY = 0;
private int offsetX = 0;

int startY = 0;
int startX = 0;

enum ORIENTATION {
HORIZONTAL, VERTICAL, NULL
}

private ORIENTATION mOrientation = ORIENTATION.HORIZONTAL;

public void setUpRecycleView(RecyclerView recycleView) {
if (recycleView == null) {
throw new IllegalArgumentException(“recycleView must be not null”);
}
mRecyclerView = recycleView;
//处理滑动
recycleView.setOnFlingListener(mOnFlingListener);
//设置滚动监听,记录滚动的状态,和总的偏移量
recycleView.setOnScrollListener(mOnScrollListener);
//记录滚动开始的位置
recycleView.setOnTouchListener(mOnTouchListener);
//获取滚动的方向
updateLayoutManger();

}

public void updateLayoutManger() {
RecyclerView.LayoutManager layoutManager = mRecyclerView.getLayoutManager();
if (layoutManager != null) {
if (layoutManager.canScrollVertically()) {
mOrientation = ORIENTATION.VERTICAL;
} else if (layoutManager.canScrollHorizontally()) {
mOrientation = ORIENTATION.HORIZONTAL;
} else {
mOrientation = ORIENTATION.NULL;
}
if (mAnimator != null) {
mAnimator.cancel();
}
startX = 0;
startY = 0;
offsetX = 0;
offsetY = 0;

}

}

/**
* 获取总共的页数
*/
public int getPageCount() {
if (mRecyclerView != null) {
if (mOrientation == ORIENTATION.NULL) {
return 0;
}
if (mOrientation == ORIENTATION.VERTICAL && mRecyclerView.computeVerticalScrollExtent() != 0) {
return mRecyclerView.computeVerticalScrollRange() / mRecyclerView.computeVerticalScrollExtent();
} else if (mRecyclerView.computeHorizontalScrollExtent() != 0) {
Log.i(“zzz”,”rang=”+mRecyclerView.computeHorizontalScrollRange()+” extent=”+mRecyclerView.computeHorizontalScrollExtent());
return mRecyclerView.computeHorizontalScrollRange() / mRecyclerView.computeHorizontalScrollExtent();
}
}
return 0;
}

ValueAnimator mAnimator = null;

public void scrollToPosition(int position) {
if (mAnimator == null) {
mOnFlingListener.onFling(0, 0);
}
if (mAnimator != null) {
int startPoint = mOrientation == ORIENTATION.VERTICAL ? offsetY : offsetX, endPoint = 0;
if (mOrientation == ORIENTATION.VERTICAL) {
endPoint = mRecyclerView.getHeight() * position;
} else {
endPoint = mRecyclerView.getWidth() * position;
}
if (startPoint != endPoint) {
mAnimator.setIntValues(startPoint, endPoint);
mAnimator.start();
}
}
}

public class MyOnFlingListener extends RecyclerView.OnFlingListener {

@Override
public boolean onFling(int velocityX, int velocityY) {
if (mOrientation == ORIENTATION.NULL) {
return false;
}
//获取开始滚动时所在页面的index
int p = getStartPageIndex();

//记录滚动开始和结束的位置
int endPoint = 0;
int startPoint = 0;

//如果是垂直方向
if (mOrientation == ORIENTATION.VERTICAL) {
startPoint = offsetY;

if (velocityY < 0) {
p–;
} else if (velocityY > 0) {
p++;
}
//更具不同的速度判断需要滚动的方向
//注意,此处有一个技巧,就是当速度为0的时候就滚动会开始的页面,即实现页面复位
endPoint = p * mRecyclerView.getHeight();

} else {
startPoint = offsetX;
if (velocityX < 0) {
p–;
} else if (velocityX > 0) {
p++;
}
endPoint = p * mRecyclerView.getWidth();

}
if (endPoint < 0) {
endPoint = 0;
}

//使用动画处理滚动
if (mAnimator == null) {
mAnimator = new ValueAnimator().ofInt(startPoint, endPoint);

mAnimator.setDuration(300);
mAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
int nowPoint = (int) animation.getAnimatedValue();

if (mOrientation == ORIENTATION.VERTICAL) {
int dy = nowPoint – offsetY;
//这里通过RecyclerView的scrollBy方法实现滚动。
mRecyclerView.scrollBy(0, dy);
} else {
int dx = nowPoint – offsetX;
mRecyclerView.scrollBy(dx, 0);
}
}
});
mAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
//回调监听
if (null != mOnPageChangeListener) {
mOnPageChangeListener.onPageChange(getPageIndex());
}
//修复双击item bug
mRecyclerView.stopScroll();
startY = offsetY;
startX = offsetX;
}
});
} else {
mAnimator.cancel();
mAnimator.setIntValues(startPoint, endPoint);
}

mAnimator.start();

return true;
}
}

public class MyOnScrollListener extends RecyclerView.OnScrollListener {
@Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
//newState==0表示滚动停止,此时需要处理回滚
if (newState == 0 && mOrientation != ORIENTATION.NULL) {
boolean move;
int vX = 0, vY = 0;
if (mOrientation == ORIENTATION.VERTICAL) {
int absY = Math.abs(offsetY – startY);
//如果滑动的距离超过屏幕的一半表示需要滑动到下一页
move = absY > recyclerView.getHeight() / 2;
vY = 0;

if (move) {
vY = offsetY – startY < 0 ? -1000 : 1000;
}

} else {
int absX = Math.abs(offsetX – startX);
move = absX > recyclerView.getWidth() / 2;
if (move) {
vX = offsetX – startX < 0 ? -1000 : 1000;
}

}

mOnFlingListener.onFling(vX, vY);

}

}

@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
//滚动结束记录滚动的偏移量
offsetY += dy;
offsetX += dx;
}
}

private MyOnTouchListener mOnTouchListener = new MyOnTouchListener();

private boolean firstTouch = true;

public class MyOnTouchListener implements View.OnTouchListener {

@Override
public boolean onTouch(View v, MotionEvent event) {
//手指按下的时候记录开始滚动的坐标
if (firstTouch) {
//*次touch可能是ACTION_MOVE或ACTION_DOWN,所以使用这种方式判断
firstTouch = false;
startY = offsetY;
startX = offsetX;
}
if (event.getAction() == MotionEvent.ACTION_UP || event.getAction() == MotionEvent.ACTION_CANCEL) {
firstTouch = true;
}

return false;
}

}

private int getPageIndex() {
int p = 0;
if (mRecyclerView.getHeight() == 0 || mRecyclerView.getWidth() == 0) {
return p;
}
if (mOrientation == ORIENTATION.VERTICAL) {
p = offsetY / mRecyclerView.getHeight();
} else {
p = offsetX / mRecyclerView.getWidth();
}
return p;
}

private int getStartPageIndex() {
int p = 0;
if (mRecyclerView.getHeight() == 0 || mRecyclerView.getWidth() == 0) {
//没有宽高无法处理
return p;
}
if (mOrientation == ORIENTATION.VERTICAL) {
p = startY / mRecyclerView.getHeight();
} else {
p = startX / mRecyclerView.getWidth();
}
return p;
}

onPageChangeListener mOnPageChangeListener;

public void setOnPageChangeListener(onPageChangeListener listener) {
mOnPageChangeListener = listener;
}

public interface onPageChangeListener {
void onPageChange(int index);
}

}
四:初始化RecyclerView同时设置为横向滑动

//使用通用RecyclerView组件
PagingScrollHelper scrollHelper = new PagingScrollHelper();//初始化横向管理器
HorizontalPageLayoutManager horizontalPageLayoutManager = new HorizontalPageLayoutManager(1, 3);//这里两个参数是行列,这里实现的是一行三列
titleAdapter = new RsdTitleAdapter(this);//设置适配器
mrecRsd.setAdapter(titleAdapter);
scrollHelper.setUpRecycleView(mrecRsd);//将横向布局管理器和recycler view绑定到一起
scrollHelper.setOnPageChangeListener(this);//设置滑动监听
mrecRsd.setLayoutManager(horizontalPageLayoutManager);//设置为横向
scrollHelper.updateLayoutManger();
scrollHelper.scrollToPosition(0);//默认滑动到*页
mrecRsd.setHorizontalScrollBarEnabled(true);
五:页面滑动监听

//页面切换
@Override
public void onPageChange(int index) {
//这里是配合圆点指示器实现的,可以忽略
for (int i = 0; i < dotViews.length; i++) {
if (index == i) {
dotViews[i].setSelected(true);
dotViews[i].setImageResource(R.drawable.red_circle);
} else {
dotViews[i].setSelected(false);
dotViews[i].setImageResource(R.drawable.gry_circle);
}
}
}
2019/8/13更新 添加圆点指示器代码
private ImageView[] dotViews;//创建存放图片集合

//生成相应数量的导航小圆点
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT);
//设置小圆点左右之间的间隔
params.setMargins(10, 0, 10, 0);
//得到页面个数
dotViews = new ImageView[6];//我这里是固定的六页 也可以根据自己需要设置圆点个数
for (int i = 0; i < 6; i++) {
ImageView imageView = new ImageView(getContext());
imageView.setLayoutParams(params);
imageView.setImageResource(R.drawable.gry_circle);//初始化六个灰色Img
if (i == 0) {
//默认启动时,选中*个小圆点
imageView.setSelected(true);
} else {
imageView.setSelected(false);//其他的设置不选择
}
//得到每个小圆点的引用,用于滑动页面时,(onPageSelected方法中)更改它们的状态。
dotViews[i] = imageView;
dotViews[0].setImageResource(R.drawable.red_circle);//设置*个页面选择
//添加到布局里面显示
img_layout.addView(imageView);//这里的img_layout就是我在布局中写的一个Linear Layout用来存放这些圆点img

使用RecyclerView实现多行水平分页的GridView效果和ViewPager效果

前些天看到有人在论坛上问这种效果怎么实现,没写过也没用过这个功能,网上查了一下,大多是使用ViewPager+GridView或者HorizontalScrollView+GridView实现,不过貌似有点复杂,太懒,没仔细看。这两天学习RecyclerView的使用(网上有很多文章,建议大家阅读本博客的时候先去了解一下),发现RecyclerView可以实现GridView 的横向滚动效果,不过没有分页,因此决定自己写一个。

博文*后也有贴出完整代码,使用Eclipse的同学可以自己新建项目并Copy代码。

效果图:
(由于这里每个Item都很相像,所以效果看起来不是很好,请见谅)
图1:
在这里插入图片描述
图2:
在这里插入图片描述
(删除的操作是在长按事件中写的)
图1是带页码指示器的多行横向分页的GridView效果,拖动距离不足时,还可以滚动回原来的位置(类似于ViewPager拖动距离不足的效果);
图2是和ViewPager一模一样的效果,实现此效果只要设置行数和列数都为1即可。

代码结构:

在这里插入图片描述

  • AutoGridLayoutManager继承自GridLayoutManager并重写了onMeasure方法,目的是使RecyclerView的高度自适应内容高度。
  • DimensionConvert是一个用来转换px和pd的工具类。
  • MainActivity是一个使用示例。
  • PageIndicatorView继承自LinearLayout,存放一些小圆点作为页码指示器。
  • PageRecyclerView继承自RecyclerView,用来完成分页等功能。

先简单讲一下实现步骤,之后贴完整的代码

*步: 实现横向滚动的GridView效果

这个很简单,只要给RecyclerView设置横向的GridLayoutManager就可以了。但是使用过程中发现,RecyclerView并不会自适应内容的高度,因此重写了GridLayoutManager的onMeasure方法(MyGridLayoutManager.java);

@Override
public void onMeasure(RecyclerView.Recycler recycler, RecyclerView.State state, int widthSpec, int heightSpec) {
    View view = recycler.getViewForPosition(0);
    if (view != null) {
        measureChild(view, widthSpec, heightSpec);
        int measuredWidth = View.MeasureSpec.getSize(widthSpec);
        int measuredHeight = view.getMeasuredHeight() * getSpanCount();
        setMeasuredDimension(measuredWidth, measuredHeight);
    }
}

 

第二步:实现自定义行数和列数功能

实现此功能需要重写RecyclerView(MyRecyclerView.java),并添加两个成员变量spanRowspanColumn和一个设置行数列数的方法setPageSize(int spanRow, int spanColumn)
之后,在Adapter中生成Item的时候就可以根据设置好的PageRecyclerView的宽度和列数计算单个Item的宽度,以达到一页正好显示固定列数的目的:

@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
    if (itemWidth <= 0) {
        // 计算Item的宽度
        itemWidth = (parent.getWidth() - pageMargin * 2) / spanColumn;
    }

    RecyclerView.ViewHolder holder = mCallBack.onCreateViewHolder(parent, viewType);

    holder.itemView.measure(0, 0);
    holder.itemView.getLayoutParams().width = itemWidth;
    holder.itemView.getLayoutParams().height = holder.itemView.getMeasuredHeight();

    return holder;
}

 

可以看到上面代码中有一个mCallBack变量,这是一个接口的实现类的实例,我们需要创建Adapter实例的时候传入一个此接口的子类实例。

public interface CallBack {

    /**
     * 创建VieHolder
     *
     * @param parent
     * @param viewType
     */
    RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType);

    /**
     * 绑定数据到ViewHolder
     *
     * @param holder
     * @param position
     */
    void onBindViewHolder(RecyclerView.ViewHolder holder, int position);

}

 

此接口共有两个方法,这两个方法和Adapter中需要重写的两个方法一样,用法也一样,分别用来创建ViewHolder实例和给ViewHolder中的控件绑定数据。

第三步:开始分页滚动

1> 分页:
完成第二步之后,布局就调整好了,之后我们实现分页滚动的功能。要分页就肯定需要总页数(totalPage)和当前页码(currentPage),我们需要在设置Adapter适配器之后根据Item的总数和每页的Item数计算总页数:

@Override
public void setAdapter(Adapter adapter) {
    super.setAdapter(adapter);
    // 计算总页数
    totalPage = ((int) Math.ceil(adapter.getItemCount() / (double) (spanRow * spanColumn)));
    mIndicatorView.initIndicator(totalPage);
}

 

然后就可以重写RecyclerView的onTouchEvent方法实现分页,根据ACTION_DOWNACTION_UP时候的坐标计算滑动方向,在ACTION_UP的时候根据滑动的方向使用smoothScrollBy方法向左或向右滑动一个MyRecyclerView的宽度就可以了。
不过这种切换页面的方式很生硬,我们要实现的ViewPager的滑动效果:要滑动超过一定的距离才能切换页码,否则滚回原来的位置。实现此功能需要一个常量,不过为了适应各种宽度的MyRecyclerView,这里根据MyRecyclerView的宽度动态设置*小滚动距离:

private int shortestDistance; // 超过此距离的滑动才有效

@Override
protected void onMeasure(int widthSpec, int heightSpec) {
    super.onMeasure(widthSpec, heightSpec);
    shortestDistance = getMeasuredWidth() / 3;
}

 

还需要其他的几个变量:

private float downX = 0; // 手指按下的X轴坐标
private float slideDistance = 0; // 滑动的距离
private float scrollX = 0; // X轴当前的位置

 

scrollX为当前滚动的位置,重写onScrolled计算滚动到的位置:

@Override
public void onScrolled(int dx, int dy) {
    scrollX += dx;
    super.onScrolled(dx, dy);
}

 

之后就可以编写完整的onTouchEvent方法

@Override
public boolean onTouchEvent(MotionEvent event) {

    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            downX = event.getX();
            break;
        case MotionEvent.ACTION_UP:
            slideDistance = event.getX() - downX;
            if (Math.abs(slideDistance) > shortestDistance) {
                // 滑动距离足够,执行翻页
                if (slideDistance > 0) {
                    // 上一页
                    currentPage = currentPage == 1 ? 1 : currentPage - 1;
                } else {
                    // 下一页
                    currentPage = currentPage == totalPage ? totalPage : currentPage + 1;
                }
            }
            // 执行滚动
            smoothScrollBy((int) ((currentPage - 1) * getWidth() - scrollX), 0);
            return true;
        default:
            break;
    }

    return super.onTouchEvent(event);
}

2> 页间距

为了分页更加清晰,还需要给页与页添加间距:
首先添加一个成员变量,和set方法

private int pageMargin = 0; // 页间距
/**
 * 设置页间距
 *
 * @param pageMargin 间距(px)
 */
public void setPageMargin(int pageMargin) {
    this.pageMargin = pageMargin;
}

 

然后重写Adapter的onBindViewHolder方法调整页间距:

@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
    if (spanColumn == 1) {
        // 每个Item距离左右两侧各pageMargin
        holder.itemView.getLayoutParams().width = itemWidth + pageMargin * 2;
        holder.itemView.setPadding(pageMargin, 0, pageMargin, 0);
    } else {
        int m = position % (spanRow * spanColumn);
        if (m < spanRow) {
            // 每页左侧的Item距离左边pageMargin
            holder.itemView.getLayoutParams().width = itemWidth + pageMargin;
            holder.itemView.setPadding(pageMargin, 0, 0, 0);
        } else if (m >= spanRow * spanColumn - spanRow) {
            // 每页右侧的Item距离右边pageMargin
            holder.itemView.getLayoutParams().width = itemWidth + pageMargin;
            holder.itemView.setPadding(0, 0, pageMargin, 0);
        } else {
            // 中间的正常显示
            holder.itemView.getLayoutParams().width = itemWidth;
            holder.itemView.setPadding(0, 0, 0, 0);
        }
    }

}

 

3> 占位Item

为了*后不足一页时也能完整显示,还需要在*后不足一页时,生成占位的View,因此修改Adapter的onBindViewHolder方法和getItemCount方法:

@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {

    ...

    if (position < dataList.size()) {
        holder.itemView.setAlpha(1);
        mCallBack.onBindViewHolder(holder, position);
    } else {
        holder.itemView.setAlpha(0);
    }
}

@Override
public int getItemCount() {
    int m = dataList.size() % (spanRow * spanColumn);
    if (m == 0) {
        return dataList.size();
    } else {
       return dataList.size() + (spanRow * spanColumn - m);
   }
}

至此,分页功能就完成了,为了功能更丰满,还需要添加一个分页指示器(就是效果图中的小圆点),这个功能还是很简单的,新建一个类继承LinearLayout并根据总页数生成一些小圆点的View,然后提供一个修改当前页码的方法就OK啦。

第四步:删除Item

*后还有一个删除Item的功能,实现方式还是使用系统的Adapter的notifyItemRemoved(int position);方法,由于前面分页时给部分Item设置了padding,所以为了布局不会错乱,还需要更新其他改变的Item:

// 删除Item
notifyItemRemoved(position);
// 更新界面上发生改变的Item
notifyItemRangeChanged(position, currentPage * spanRow * spanColumn);

 

然后还要更新页码指示器,这里就不贴代码了,直接看下面的类就可以了。
使用的时候只要把指示器和MyRecyclerView按照自己的需求布局,并在切换页面的时候更新指示器就完成了。

改:

  1. 上面分页滑动是在onTouchEvent()方法中实现的,但是后来发现,这种实现方式会导致给Item添加onClickListeneronLongClickListeneronTouchListener的时候会产生事件冲突,因此修改为在onScrollStateChanged()方法中实现,代码如下:
/*
     * 0: 停止滚动且手指移开; 1: 开始滚动; 2: 手指做了抛的动作(手指离开屏幕前,用力滑了一下)
     */
private int scrollState = 0; // 滚动状态
@Override
public void onScrollStateChanged(int state) {
    switch (state) {
        case 2:
            scrollState = 2;
            break;
        case 1:
            scrollState = 1;
            break;
        case 0:
            if (slideDistance == 0) {
                break;
            }
            scrollState = 0;
            if (slideDistance < 0) { // 上页
                currentPage = (int) Math.ceil(scrollX / getWidth());
                if (currentPage * getWidth() - scrollX < shortestDistance) {
                    currentPage += 1;
                }
            } else { // 下页
                currentPage = (int) Math.ceil(scrollX / getWidth()) + 1;
                if (currentPage <= totalPage) {
                    if (scrollX - (currentPage - 2) * getWidth() < shortestDistance) {
                        // 如果这一页滑出距离不足,则定位到前一页
                        currentPage -= 1;
                    }
                } else {
                    currentPage = totalPage;
                }
            }
            // 执行自动滚动
            smoothScrollBy((int) ((currentPage - 1) * getWidth() - scrollX), 0);
            // 修改指示器选中项
            mIndicatorView.setSelectedPage(currentPage - 1);
            slideDistance = 0;
            break;
    }
    super.onScrollStateChanged(state);
}

@Override
public void onScrolled(int dx, int dy) {
    scrollX += dx;
    if (scrollState == 1) {
        slideDistance += dx;
    }

    super.onScrolled(dx, dy);
}

 

RecyclerView的GridLayoutManager是从上到下从左到右排列的,而我们分页时大多需要的是从左到右从上到下排列,因此增加一个方法调整位置(此方法只适用于3*3排列的,还没有找到通用的方法,如果那位同学有方法,麻烦分享一下,先谢过)

private void countRealPosition(int position) {
    // 为了使Item从左到右从上到下排列,需要position的值
    int m = position % (spanRow * spanColumn);
    switch (m) {
        case 1:
        case 5:
            realPosition = position + 2;
            break;
        case 3:
        case 7:
            realPosition = position - 2;
            break;
        case 2:
            realPosition = position + 4;
            break;
        case 6:
            realPosition = position - 4;
            break;
        case 0:
        case 4:
        case 8:
            realPosition = position;
            break;
    }
}

 

<<<<<<<<<<<<<<<<<<<<<<使用方法参考MainActivity.java>>>>>>>>>>>>>>>>>>>>

上面讲的不够详细,具体见代码>>>>>>>>>>>>>>

完整代码:

AutoGridLayoutManager.java

使用这个类替代GridLayoutManager是为了使RecyclerView及其子类能够自适应内容的高度。

import android.content.Context;
import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.util.AttributeSet;
import android.view.View;

/**
 * Created by shichaohui on 2015/7/9 0009.
 * <p>
 * 重写GridLayoutManager,在{@link RecyclerView#setLayoutManager(RecyclerView.LayoutManager)}使用
 * 此类替换{@link GridLayoutManager},使{@link RecyclerView}能够自使用内容的高度
 * </p>
 */
public class AutoGridLayoutManager extends GridLayoutManager {

    private int measuredWidth = 0;
    private int measuredHeight = 0;

    public AutoGridLayoutManager(Context context, AttributeSet attrs,
                                 int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }

    public AutoGridLayoutManager(Context context, int spanCount) {
        super(context, spanCount);
    }

    public AutoGridLayoutManager(Context context, int spanCount,
                                 int orientation, boolean reverseLayout) {
        super(context, spanCount, orientation, reverseLayout);
    }

    @Override
    public void onMeasure(RecyclerView.Recycler recycler,
                          RecyclerView.State state, int widthSpec, int heightSpec) {
        if (measuredHeight <= 0) {
            View view = recycler.getViewForPosition(0);
            if (view != null) {
                measureChild(view, widthSpec, heightSpec);
                measuredWidth = View.MeasureSpec.getSize(widthSpec);
                measuredHeight = view.getMeasuredHeight() * getSpanCount();
            }
        }
        setMeasuredDimension(measuredWidth, measuredHeight);
    }

}

 

PageRecyclerView.java

重写RecyclerView实现分页

import android.content.Context;
import android.graphics.Color;
import android.support.v4.view.PagerAdapter;
import android.support.v7.widget.RecyclerView;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;

import java.util.List;
import java.util.Objects;

/**
 * Created by shichaohui on 2015/7/9 0009.
 * <p>
 * 横向分页的GridView效果
 * </p>
 * <p>
 * 默认为1行,每页3列,如果要自定义行数和列数,请在调用{@link PageRecyclerView#setAdapter(Adapter)}方法前调用
 * {@link PageRecyclerView#setPageSize(int, int)}方法自定义行数
 * </p>
 */
public class PageRecyclerView extends RecyclerView {

    private Context mContext = null;

    private PageAdapter myAdapter = null;

    private int shortestDistance; // 超过此距离的滑动才有效
    private float downX = 0; // 手指按下的X轴坐标
    private float slideDistance = 0; // 滑动的距离
    private float scrollX = 0; // X轴当前的位置

    private int spanRow = 1; // 行数
    private int spanColumn = 3; // 每页列数
    private int totalPage = 0; // 总页数
    private int currentPage = 1; // 当前页

    private int pageMargin = 0; // 页间距

    private PageIndicatorView mIndicatorView = null; // 指示器布局

    public PageRecyclerView(Context context) {
        this(context, null);
    }

    public PageRecyclerView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public PageRecyclerView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        defaultInit(context);
    }

    // 默认初始化
    private void defaultInit(Context context) {
        this.mContext = context;
        setLayoutManager(new AutoGridLayoutManager(
                mContext, spanRow, AutoGridLayoutManager.HORIZONTAL, false));
        setOverScrollMode(OVER_SCROLL_NEVER);
    }

    /**
     * 设置行数和每页列数
     *
     * @param spanRow    行数,<=0表示使用默认的行数
     * @param spanColumn 每页列数,<=0表示使用默认每页列数
     */
    public void setPageSize(int spanRow, int spanColumn) {
        this.spanRow = spanRow <= 0 ? this.spanRow : spanRow;
        this.spanColumn = spanColumn <= 0 ? this.spanColumn : spanColumn;
        setLayoutManager(new AutoGridLayoutManager(
                mContext, this.spanRow, AutoGridLayoutManager.HORIZONTAL, false));
    }

    /**
     * 设置页间距
     *
     * @param pageMargin 间距(px)
     */
    public void setPageMargin(int pageMargin) {
        this.pageMargin = pageMargin;
    }

    /**
     * 设置指示器
     *
     * @param indicatorView 指示器布局
     */
    public void setIndicator(PageIndicatorView indicatorView) {
        this.mIndicatorView = indicatorView;
    }

    @Override
    protected void onMeasure(int widthSpec, int heightSpec) {
        super.onMeasure(widthSpec, heightSpec);
        shortestDistance = getMeasuredWidth() / 3;
    }

    @Override
    public void setAdapter(Adapter adapter) {
        super.setAdapter(adapter);
        this.myAdapter = (PageAdapter) adapter;
        update();
    }

    // 更新页码指示器和相关数据
    private void update() {
        // 计算总页数
        int temp = ((int) Math.ceil(myAdapter.dataList.size() / (double) (spanRow * spanColumn)));
        if (temp != totalPage) {
            mIndicatorView.initIndicator(temp);
            // 页码减少且当前页为*后一页
            if (temp < totalPage && currentPage == totalPage) {
                currentPage = temp;
                // 执行滚动
                smoothScrollBy(-getWidth(), 0);
            }
            mIndicatorView.setSelectedPage(currentPage - 1);
            totalPage = temp;
        }
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {

        switch (event.getAction()) {
            case MotionEvent.ACTION_MOVE:
                if (currentPage == totalPage && downX - event.getX() > 0) {
                    return true;
                }
                break;
            case MotionEvent.ACTION_DOWN:
                downX = event.getX();
                break;
            case MotionEvent.ACTION_UP:
                slideDistance = event.getX() - downX;
                if (Math.abs(slideDistance) > shortestDistance) {
                    // 滑动距离足够,执行翻页
                    if (slideDistance > 0) {
                        // 上一页
                        currentPage = currentPage == 1 ? 1 : currentPage - 1;
                    } else {
                        // 下一页
                        currentPage = currentPage == totalPage ? totalPage : currentPage + 1;
                    }
                    // 修改指示器选中项
                    mIndicatorView.setSelectedPage(currentPage - 1);
                }
                // 执行滚动
                smoothScrollBy((int) ((currentPage - 1) * getWidth() - scrollX), 0);
                return true;
            default:
                break;
        }

        return super.onTouchEvent(event);
    }

    @Override
    public void onScrolled(int dx, int dy) {
        scrollX += dx;
        super.onScrolled(dx, dy);
    }

    /**
     * 数据适配器
     */
    public class PageAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {

        private List<?> dataList = null;
        private CallBack mCallBack = null;
        private int itemWidth = 0;
        private int itemCount = 0;

        /**
         * 实例化适配器
         *
         * @param data
         * @param callBack
         */
        public PageAdapter(List<?> data, CallBack callBack) {
            this.dataList = data;
            this.mCallBack = callBack;
            itemCount = dataList.size() + spanRow * spanColumn;
        }

        @Override
        public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
            if (itemWidth <= 0) {
                // 计算Item的宽度
                itemWidth = (parent.getWidth() - pageMargin * 2) / spanColumn;
            }

            RecyclerView.ViewHolder holder = mCallBack.onCreateViewHolder(parent, viewType);

            holder.itemView.measure(0, 0);
            holder.itemView.getLayoutParams().width = itemWidth;
            holder.itemView.getLayoutParams().height = holder.itemView.getMeasuredHeight();

            return holder;
        }

        @Override
        public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
            if (spanColumn == 1) {
                // 每个Item距离左右两侧各pageMargin
                holder.itemView.getLayoutParams().width = itemWidth + pageMargin * 2;
                holder.itemView.setPadding(pageMargin, 0, pageMargin, 0);
            } else {
                int m = position % (spanRow * spanColumn);
                if (m < spanRow) {
                    // 每页左侧的Item距离左边pageMargin
                    holder.itemView.getLayoutParams().width = itemWidth + pageMargin;
                    holder.itemView.setPadding(pageMargin, 0, 0, 0);
                } else if (m >= spanRow * spanColumn - spanRow) {
                    // 每页右侧的Item距离右边pageMargin
                    holder.itemView.getLayoutParams().width = itemWidth + pageMargin;
                    holder.itemView.setPadding(0, 0, pageMargin, 0);
                } else {
                    // 中间的正常显示
                    holder.itemView.getLayoutParams().width = itemWidth;
                    holder.itemView.setPadding(0, 0, 0, 0);
                }
            }

            if (position < dataList.size()) {
                holder.itemView.setVisibility(View.VISIBLE);
                mCallBack.onBindViewHolder(holder, position);
            } else {
                holder.itemView.setVisibility(View.INVISIBLE);
            }

        }

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

        /**
         * 删除Item
         * @param position 位置
         */
        public void remove(int position) {
            if (position < dataList.size()) {
                // 删除数据
                dataList.remove(position);
                itemCount--;
                // 删除Item
                notifyItemRemoved(position);
                // 更新界面上发生改变的Item
                notifyItemRangeChanged(position, currentPage * spanRow * spanColumn);
                // 更新页码指示器
                update();
            }
        }

    }

    public interface CallBack {

        /**
         * 创建VieHolder
         *
         * @param parent
         * @param viewType
         */
        RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType);

        /**
         * 绑定数据到ViewHolder
         *
         * @param holder
         * @param position
         */
        void onBindViewHolder(RecyclerView.ViewHolder holder, int position);

    }

}

 

PageIndicatorView.java

页码指示器 ,此类可以作为一个工具类,在ViewPager做的轮播图上也可以使用

import android.content.Context;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.View;
import android.widget.LinearLayout;

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

/**
 * Created by shichaohui on 2015/7/10 0010.
 * <p/>
 * 页码指示器类,获得此类实例后,可通过{@link PageIndicatorView#initIndicator(int)}方法初始化指示器
 * </P>
 */
public class PageIndicatorView extends LinearLayout {

    private Context mContext = null;
    private int dotSize = 15; // 指示器的大小(dp)
    private int margins = 4; // 指示器间距(dp)
    private List<View> indicatorViews = null; // 存放指示器

    public PageIndicatorView(Context context) {
        this(context, null);
    }

    public PageIndicatorView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public PageIndicatorView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context);
    }

    private void init(Context context) {
        this.mContext = context;

        setGravity(Gravity.CENTER);
        setOrientation(HORIZONTAL);

        dotSize = DimensionConvert.dip2px(context, dotSize);
        margins = DimensionConvert.dip2px(context, margins);
    }

    /**
     * 初始化指示器,默认选中*页
     *
     * @param count 指示器数量,即页数
     */
    public void initIndicator(int count) {

        if (indicatorViews == null) {
            indicatorViews = new ArrayList<>();
        } else {
            indicatorViews.clear();
            removeAllViews();
        }
        View view;
        LayoutParams params = new LayoutParams(dotSize, dotSize);
        params.setMargins(margins, margins, margins, margins);
        for (int i = 0; i < count; i++) {
            view = new View(mContext);
            view.setBackgroundResource(android.R.drawable.presence_invisible);
            addView(view, params);
            indicatorViews.add(view);
        }
        if (indicatorViews.size() > 0) {
            indicatorViews.get(0).setBackgroundResource(android.R.drawable.presence_online);
        }
    }

    /**
     * 设置选中页
     *
     * @param selected 页下标,从0开始
     */
    public void setSelectedPage(int selected) {
        for (int i = 0; i < indicatorViews.size(); i++) {
            if (i == selected) {
                indicatorViews.get(i).setBackgroundResource(android.R.drawable.presence_online);
            } else {
                indicatorViews.get(i).setBackgroundResource(android.R.drawable.presence_invisible);
            }
        }
    }

}

 

DimensionConvert.java

用来转换dip和px的工具类

import android.content.Context;

/**
 * Created by shichaohui on 2015/7/10 0010.
 */
public class DimensionConvert {

    /**
     * 根据手机的分辨率从 dp 的单位 转成为 px(像素)
     *
     * @param context
     * @param dpValue 要转换的dp值
     */
    public static int dip2px(Context context, float dpValue) {
        final float scale = context.getResources().getDisplayMetrics().density;
        return (int) (dpValue * scale + 0.5f);
    }

    /**
     * 根据手机的分辨率从 px(像素) 的单位 转成为 dp
     *
     * @param context
     * @param pxValue 要转换的px值
     */
    public static int px2dip(Context context, float pxValue) {
        final float scale = context.getResources().getDisplayMetrics().density;
        return (int) (pxValue / scale + 0.5f);
    }
}

 

MainActivity.java

import android.app.Activity;
import android.os.Bundle;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import android.widget.Toast;

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

public class MainActivity extends Activity {

    private PageRecyclerView mRecyclerView = null;
    private List<String> dataList = null;
    private PageRecyclerView.PageAdapter myAdapter = null;

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

        setContentView(R.layout.activity_main);

        initData();

        mRecyclerView = (PageRecyclerView) findViewById(R.id.cusom_swipe_view);
        // 设置指示器
        mRecyclerView.setIndicator((PageIndicatorView) findViewById(R.id.indicator));
        // 设置行数和列数
        mRecyclerView.setPageSize(3, 3);
        // 设置页间距
        mRecyclerView.setPageMargin(30);
        // 设置数据
        mRecyclerView.setAdapter(myAdapter = mRecyclerView.new PageAdapter(dataList, new PageRecyclerView.CallBack() {
            @Override
            public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
                View view = LayoutInflater.from(MainActivity.this).inflate(R.layout.item, parent, false);
                return new MyHolder(view);
            }

            @Override
            public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
                ((MyHolder)holder).tv.setText(dataList.get(position));
            }
        }));

    }

    private void initData() {
        dataList = new ArrayList<>();
        for (int i = 0; i < 50; i++) {
            dataList.add(String.valueOf(i));
        }
    }

    public class MyHolder extends RecyclerView.ViewHolder {

        public TextView tv = null;

        public MyHolder(View itemView) {
            super(itemView);
            tv = (TextView) itemView.findViewById(R.id.text);
            tv.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    Toast.makeText(MainActivity.this, getAdapterPosition() + "", Toast.LENGTH_SHORT).show();
                }
            });
            tv.setOnLongClickListener(new View.OnLongClickListener() {
                @Override
                public boolean onLongClick(View v) {
                    myAdapter.remove(getAdapterPosition());
                    return true;
                }
            });
        }
    }  *后是两个布局文件:

activity_main.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:orientation="vertical"
    android:layout_height="match_parent">

        <com.example.sch.myapplication.PageRecyclerView
            android:id="@+id/cusom_swipe_view"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />

        <com.example.sch.myapplication.PageIndicatorView
            android:id="@+id/indicator"
            android:layout_width="match_parent"
            android:layout_marginBottom="20dp"
            android:layout_height="wrap_content"
            android:layout_gravity="bottom"/>

</LinearLayout>

 

item.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:id="@+id/text"
        android:layout_width="match_parent"
        android:layout_height="50dp"
        android:layout_margin="10dp"
        android:background="#770000ff"
        android:gravity="center" />

</LinearLayout>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

END

Android RecyclerView水平分页滑动讲解

使用RecyclerView实现GridView和ViewPager滑动的分页效果,与上篇GridView分页滑动效果相似。

效果图:

%title插图%num

简单的只能直接贴图全部代码了:

1.主函数代码:

package com.yechaoa.materialdesign.activity.recyclerview;

import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.Animation;
import android.view.animation.AnimationSet;
import android.view.animation.AnimationUtils;
import android.view.animation.LayoutAnimationController;
import android.widget.TextView;
import android.widget.Toast;

import com.yechaoa.materialdesign.R;

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

/**
* 分页滑动效果
*/
public class RecycleViewActivity extends AppCompatActivity {
private PageRecyclerView mRecyclerView = null;
private List<String> dataList = null;
private PageRecyclerView.PageAdapter myAdapter = null;

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

setContentView(R.layout.activity_recycleview);

initData();

mRecyclerView = (PageRecyclerView) findViewById(R.id.cusom_swipe_view);
// 设置指示器
mRecyclerView.setIndicator((PageIndicatorView) findViewById(R.id.indicator));
// 设置行数和列数
mRecyclerView.setPageSize(3, 3);
// 设置页间距
mRecyclerView.setPageMargin(30);
//
Animation myAnim = AnimationUtils.loadAnimation(this, R.anim.in_from_right);
myAnim.setFillAfter(true);//android动画结束后停在结束位置

AnimationSet set = new AnimationSet(false);
set.addAnimation(myAnim); //加入动画集合
LayoutAnimationController controller = new LayoutAnimationController(set, 1);
mRecyclerView.setLayoutAnimation(controller);
//
// mRecyclerView.setLayoutManager(new AutoGridLayoutManager(RecycleViewActivity.this,dataList.size()));
// 设置数据
mRecyclerView.setAdapter(myAdapter = mRecyclerView.new PageAdapter(dataList, new PageRecyclerView.CallBack() {
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(RecycleViewActivity.this).inflate(R.layout.item, parent, false);
return new MyHolder(view);
}

@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
((MyHolder)holder).tv.setText(dataList.get(position));
}
}));

}

private void initData() {
dataList = new ArrayList<>();
for (int i = 0; i < 50; i++) {
dataList.add(String.valueOf(i));
}
}

public class MyHolder extends RecyclerView.ViewHolder {

public TextView tv = null;

public MyHolder(View itemView) {
super(itemView);
tv = (TextView) itemView.findViewById(R.id.text);
tv.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(RecycleViewActivity.this, getAdapterPosition() + “”, Toast.LENGTH_SHORT).show();
}
});
tv.setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
myAdapter.remove(getAdapterPosition());
return true;
}
});
}
}

}
2.自定义GridLayoutManager 试控件可以自适应内容的高度

import android.content.Context;
import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.util.AttributeSet;
import android.view.View;

/**
*
*
* 重写GridLayoutManager,在{@link
*RecyclerView#setLayoutManager(RecyclerView.LayoutManager)}使用
* 此类替换{@link GridLayoutManager},使{@link RecyclerView}能够自使用内容的高度
*
*/
public class AutoGridLayoutManager extends GridLayoutManager {

private int measuredWidth = 0;
private int measuredHeight = 0;

public AutoGridLayoutManager(Context context, AttributeSet attrs,
int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}

public AutoGridLayoutManager(Context context, int spanCount) {
super(context, spanCount);
}

public AutoGridLayoutManager(Context context, int spanCount,
int orientation, boolean reverseLayout) {
super(context, spanCount, orientation, reverseLayout);
}

@Override
public void onMeasure(RecyclerView.Recycler recycler,
RecyclerView.State state, int widthSpec, int heightSpec) {
//获取count判断,必须要有
int count = state.getItemCount();
if (count > 0) {
if (measuredHeight <= 0) {
View view = recycler.getViewForPosition(0);
if (view != null) {
measureChild(view, widthSpec, heightSpec);
measuredWidth = View.MeasureSpec.getSize(widthSpec);
measuredHeight = view.getMeasuredHeight() * getSpanCount();
}
}
setMeasuredDimension(measuredWidth, measuredHeight);
}else {
super.onMeasure(recycler, state, widthSpec, heightSpec);
}

}

}
3.自定义圆点指示器,ViewPager共用的控件类型:

import android.content.Context;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.View;
import android.widget.LinearLayout;

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

/**
*
* 指示器类,获得此类实例后,可通过{@link PageIndicatorView#initIndicator(int)}方法初始化指
* 示器
*
*/
public class PageIndicatorView extends LinearLayout {

private Context mContext = null;
private int dotSize = 15; // 指示器的大小(dp)
private int margins = 4; // 指示器间距(dp)
private List<View> indicatorViews = null; // 存放指示器

public PageIndicatorView(Context context) {
this(context, null);
}

public PageIndicatorView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}

public PageIndicatorView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}

private void init(Context context) {
this.mContext = context;

setGravity(Gravity.CENTER);
setOrientation(HORIZONTAL);

dotSize = DimensionConvert.dip2px(context, dotSize);
margins = DimensionConvert.dip2px(context, margins);
}

/**
* 初始化指示器,默认选中*页
*
* @param count 指示器数量,即页数
*/
public void initIndicator(int count) {

if (indicatorViews == null) {
indicatorViews = new ArrayList<>();
} else {
indicatorViews.clear();
removeAllViews();
}
View view;
LayoutParams params = new LayoutParams(dotSize, dotSize);
params.setMargins(margins, margins, margins, margins);
for (int i = 0; i < count; i++) {
view = new View(mContext);
view.setBackgroundResource(android.R.drawable.presence_invisible);
addView(view, params);
indicatorViews.add(view);
}
if (indicatorViews.size() > 0) {
indicatorViews.get(0).setBackgroundResource(android.R.drawable.presence_online);
}
}

/**
* 设置选中页
*
* @param selected 页下标,从0开始
*/
public void setSelectedPage(int selected) {
for (int i = 0; i < indicatorViews.size(); i++) {
if (i == selected) {
indicatorViews.get(i).setBackgroundResource(android.R.drawable.presence_online);
} else {
indicatorViews.get(i).setBackgroundResource(android.R.drawable.presence_invisible);
}
}
}

}
4.自定义分页RecyclerView

import android.content.Context;
import android.graphics.Color;
import android.support.v4.view.PagerAdapter;
import android.support.v7.widget.RecyclerView;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;

import java.util.List;
import java.util.Objects;

/**
* 横向分页的GridView效果
* 默认为1行,每页3列,如果要自定义行数和列数,请在调用{@link PageRecyclerView#setAdapter(Adapter)}方法前调用
* {@link PageRecyclerView#setPageSize(int, int)}方法自定义行数
*/
public class PageRecyclerView extends RecyclerView {

private Context mContext = null;

private PageAdapter myAdapter = null;

private int shortestDistance; // 超过此距离的滑动才有效
private float downX = 0; // 手指按下的X轴坐标
private float slideDistance = 0; // 滑动的距离
private float scrollX = 0; // X轴当前的位置

private int spanRow = 1; // 行数
private int spanColumn = 3; // 每页列数
private int totalPage = 0; // 总页数
private int currentPage = 1; // 当前页

private int pageMargin = 0; // 页间距

private PageIndicatorView mIndicatorView = null; // 指示器布局

public PageRecyclerView(Context context) {
this(context, null);
}

public PageRecyclerView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}

public PageRecyclerView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
defaultInit(context);
}

// 默认初始化
private void defaultInit(Context context) {
this.mContext = context;
setLayoutManager(new AutoGridLayoutManager(
mContext, spanRow, AutoGridLayoutManager.HORIZONTAL, false));
setOverScrollMode(OVER_SCROLL_NEVER);
}

/**
* 设置行数和每页列数
*
* @param spanRow 行数,<=0表示使用默认的行数
* @param spanColumn 每页列数,<=0表示使用默认每页列数
*/
public void setPageSize(int spanRow, int spanColumn) {
this.spanRow = spanRow <= 0 ? this.spanRow : spanRow;
this.spanColumn = spanColumn <= 0 ? this.spanColumn : spanColumn;
setLayoutManager(new AutoGridLayoutManager(
mContext, this.spanRow, AutoGridLayoutManager.HORIZONTAL, false));
}

/**
* 设置页间距
*
* @param pageMargin 间距(px)
*/
public void setPageMargin(int pageMargin) {
this.pageMargin = pageMargin;
}

/**
* 设置指示器
*
* @param indicatorView 指示器布局
*/
public void setIndicator(PageIndicatorView indicatorView) {
this.mIndicatorView = indicatorView;
}

@Override
protected void onMeasure(int widthSpec, int heightSpec) {
super.onMeasure(widthSpec, heightSpec);
shortestDistance = getMeasuredWidth() / 9;//3
}

@Override
public void setAdapter(Adapter adapter) {
super.setAdapter(adapter);
this.myAdapter = (PageAdapter) adapter;
update();
}

// 更新页码指示器和相关数据
private void update() {
// 计算总页数
int temp = ((int) Math.ceil(myAdapter.dataList.size() / (double) (spanRow * spanColumn)));
if (temp != totalPage) {
mIndicatorView.initIndicator(temp);
// 页码减少且当前页为*后一页
if (temp < totalPage && currentPage == totalPage) {
currentPage = temp;
// 执行滚动
smoothScrollBy(-getWidth(), 0);
}
mIndicatorView.setSelectedPage(currentPage – 1);
totalPage = temp;
}
}

@Override
public boolean onTouchEvent(MotionEvent event) {

switch (event.getAction()) {
case MotionEvent.ACTION_MOVE:
if (currentPage == totalPage && downX – event.getX() > 0) {
return true;
}
break;
case MotionEvent.ACTION_DOWN:
downX = event.getX();
break;
case MotionEvent.ACTION_UP:
slideDistance = event.getX() – downX;
if (Math.abs(slideDistance) > shortestDistance) {
// 滑动距离足够,执行翻页
if (slideDistance > 0) {
// 上一页
currentPage = currentPage == 1 ? 1 : currentPage – 1;
} else {
// 下一页
currentPage = currentPage == totalPage ? totalPage : currentPage + 1;
}
// 修改指示器选中项
mIndicatorView.setSelectedPage(currentPage – 1);
}
// 执行滚动
smoothScrollBy((int) ((currentPage – 1) * getWidth() – scrollX), 0);
return true;
default:
break;
}

return super.onTouchEvent(event);
}

@Override
public void onScrolled(int dx, int dy) {
scrollX += dx;
super.onScrolled(dx, dy);
}

/**
* 数据适配器
*/
public class PageAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {

private List<?> dataList = null;
private CallBack mCallBack = null;
private int itemWidth = 0;
private int itemCount = 0;

/**
* 实例化适配器
*
* @param data
* @param callBack
*/
public PageAdapter(List<?> data, CallBack callBack) {
this.dataList = data;
this.mCallBack = callBack;
itemCount = dataList.size() + spanRow * spanColumn;
}

@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
if (itemWidth <= 0) {
// 计算Item的宽度
itemWidth = (parent.getWidth() – pageMargin * 2) / spanColumn;
}

RecyclerView.ViewHolder holder = mCallBack.onCreateViewHolder(parent, viewType);

holder.itemView.measure(0, 0);
holder.itemView.getLayoutParams().width = itemWidth;
holder.itemView.getLayoutParams().height = holder.itemView.getMeasuredHeight();

return holder;
}

@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
if (spanColumn == 1) {
// 每个Item距离左右两侧各pageMargin
holder.itemView.getLayoutParams().width = itemWidth + pageMargin * 2;
holder.itemView.setPadding(pageMargin, 0, pageMargin, 0);
} else {
int m = position % (spanRow * spanColumn);
if (m < spanRow) {
// 每页左侧的Item距离左边pageMargin
holder.itemView.getLayoutParams().width = itemWidth + pageMargin;
holder.itemView.setPadding(pageMargin, 0, 0, 0);
} else if (m >= spanRow * spanColumn – spanRow) {
// 每页右侧的Item距离右边pageMargin
holder.itemView.getLayoutParams().width = itemWidth + pageMargin;
holder.itemView.setPadding(0, 0, pageMargin, 0);
} else {
// 中间的正常显示
holder.itemView.getLayoutParams().width = itemWidth;
holder.itemView.setPadding(0, 0, 0, 0);
}
}

if (position < dataList.size()) {
holder.itemView.setVisibility(View.VISIBLE);
mCallBack.onBindViewHolder(holder, position);
} else {
holder.itemView.setVisibility(View.INVISIBLE);
}

}

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

/**
* 删除Item
*
* @param position 位置
*/
public void remove(int position) {
if (position < dataList.size()) {
// 删除数据
dataList.remove(position);
itemCount–;
// 删除Item
notifyItemRemoved(position);
// 更新界面上发生改变的Item
notifyItemRangeChanged(position, currentPage * spanRow * spanColumn);
// 更新页码指示器
update();
}
}

}

public interface CallBack {

/**
* 创建VieHolder
*
* @param parent
* @param viewType
*/
RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType);

/**
* 绑定数据到ViewHolder
*
* @param holder
* @param position
*/
void onBindViewHolder(RecyclerView.ViewHolder holder, int position);

}

}

5.像素转换工具类:

import android.content.Context;

/**
* 像素转换工具
*/
public class DimensionConvert {

/**
* 根据手机的分辨率从 dp 的单位 转成为 px(像素)
*
* @param context
* @param dpValue 要转换的dp值
*/
public static int dip2px(Context context, float dpValue) {
final float scale = context.getResources().getDisplayMetrics().density;
return (int) (dpValue * scale + 0.5f);
}

/**
* 根据手机的分辨率从 px(像素) 的单位 转成为 dp
*
* @param context
* @param pxValue 要转换的px值
*/
public static int px2dip(Context context, float pxValue) {
final float scale = context.getResources().getDisplayMetrics().density;
return (int) (pxValue / scale + 0.5f);
}
}
6.布局:

activity_recyclerview.xml

<?xml version=”1.0″ encoding=”utf-8″?>
<LinearLayout xmlns:android=”http://schemas.android.com/apk/res/android”
android:layout_width=”match_parent”
android:orientation=”vertical”
android:layout_height=”match_parent”>

<com.yechaoa.materialdesign.activity.recyclerview.PageRecyclerView
android:id=”@+id/cusom_swipe_view”
android:layout_width=”match_parent”
android:layout_height=”match_parent” />

<com.yechaoa.materialdesign.activity.recyclerview.PageIndicatorView
android:id=”@+id/indicator”
android:layout_width=”match_parent”
android:layout_marginBottom=”20dp”
android:layout_height=”wrap_content”
android:layout_gravity=”bottom”/>

</LinearLayout>
item.xml

<?xml version=”1.0″ encoding=”utf-8″?>
<RelativeLayout xmlns:android=”http://schemas.android.com/apk/res/android”
android:layout_width=”match_parent”
android:layout_height=”match_parent”>

<ImageView
android:layout_centerHorizontal=”true”
android:src=”@drawable/topinfo_ban_bg”
android:layout_width=”wrap_content”
android:layout_height=”150dp”/>

<TextView
android:id=”@+id/text”
android:layout_centerHorizontal=”true”
android:layout_width=”match_parent”
android:layout_height=”150dp”
android:text=”MUSIC”
android:textColor=”@color/orange”
android:gravity=”center” />

</RelativeLayout>
 

为RecyclerView添加分隔线

就在昨天中午,我在简书上发布了我个人的*篇技术文档:RecyclerView系列之:RecyclerView添加Header和Footer,也很有幸,能够得到那么多人的支持,这让我迫不及待的赶紧写第二篇文章。今天我将谈谈:为RecyclerView添加分隔线。


一. 理解ListView和RecyclerView中的ChildView

在讲为Item加入分割线本质的前,先来介绍,认识一下ChildView,也就是平时我们用到的ListView,RecyclerView中的getChildAt(int position)这个返回的ChildView是哪一部分?到底是哪一部分呢?一开始的时候,我理解错了,但是经过下面两张图这么一比较,你就明白了:

%title插图%num
Item布局layout_margin == 0
%title插图%num
Item布局Layout_margin == 16dp

下面看代码的区别:
*张图的代码, 也就是每一个list_item的布局文件(下同)如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"              android:orientation="vertical"             
android:layout_width="match_parent"              
android:layout_height="50dp">    
      <TextView        
        android:id="@+id/list_item"        
        android:layout_width="match_parent" 
        android:layout_height="match_parent"        
        android:gravity="center"        
        android:textSize="20sp"        
        android:textColor="#262526"        
        android:background="#08da1d"/>
</LinearLayout>

第二张图的代码:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"              android:orientation="vertical"             
android:layout_width="match_parent"              
android:layout_height="50dp"
android:layout_margin="16dp">    
      <TextView        
        android:id="@+id/list_item"        
        android:layout_width="match_parent" 
        android:layout_height="match_parent"        
        android:gravity="center"        
        android:textSize="20sp"        
        android:textColor="#262526"        
        android:background="#08da1d"/>
</LinearLayout>

仔细看一下,它们的不同之处, 就是第二个图的代码中多了:

android:layout_margin = "16dp"

就多这一句而已。

所以到这里我们应该知道了ChildView是哪一部分了,就是图二中绿色这一部分,边距这一部分并不属于ChildView, 而是属于ChildView的布局。
这样我们了解ChildView之后,下面再来理解加入分隔线的原理就简单多了。


二. 理解加入分隔线的原理

在ListView中,Google为我们提供了SetDivider(Drawable divider)这样的方法来设置分隔线,那么在RecyclerView中,Google又为我们提供了什么样的方法去添加分隔线呢?通过查看官方文档,它,提供了:addItemDecoration(RecyclerView.ItemDecoration decor)这个方法了设置分隔线,那问题又来了,RecyclerView.ItemDecoration是什么东西呢?继续查:然后发现如下:它原来是一个类,里面封装了三个方法:
(1)void getItemOffsets ()
(2)void onDraw ()
(3)void onDrawOver ()


通过上面的三个方法,可以看出,这是要自己直接画上去,准确的说这几个方法是:添加Divider,主要是找到添加Divider的位置, 而Divider是在drawable文件中写好了的。 利用onDraw和onDrawOver都差不多,我们在创建自己的Decoration类继承RecyclerView.ItemDecoration的时候,我们只要重写getItemOffsets(),还有onDraw()和onDrawOver两者其中之一就可以了.


那getItemOffsets()方法有什么用呢?从字面意思就是Item要偏移, 由于我们在Item和Item之间加入了分隔线,线其实本质就是一个长方形,也是用户自定义的,既然线也有长宽高,就画横线来说,上面的Item加入了分隔线,那下面的Item就要往下平移,平移的量就是分隔线的高度。不理解每关系,后面看代码就容易理解了。


现在我们知道了如何添加了,就是通过画,那到底是画在哪里呢?画的位置又怎么确定呢?下面看图:

%title插图%num
分隔线的位置图

我现在拿画横线来说,从上面这个图中,我们很容易就可以看到,我们画分隔线的位置,是在每一个Item的布局之间,注意:是布局之间。

好了,我们确定了画在哪里,那我们怎么确定画线的具体的坐标位置呢?也就是我们要确定:分隔线的left, top, right, Bottom. 在Adapter中,我们很容易通过parent(这个parent它其实就是我们能看到的部分)获取每一个childView:
(1)left:parent.getPaddingLeft()
(2)right: parent. getWidth()-parent.getPaddingRight();
(3)top : 就是红线的上面:我们通过ChildView.getBottom()来得到这个Item的底部的高度,也就是蓝线位置,蓝线和红线之间间距:就是这个Item布局文件的:layout_marginBottom, 然后top的位置就是两者之和。
(4)bttom: 就是top加上分隔线的高度:top+线高


通过上面的解析,你也许知道了加入分隔线的原理,不理解也没有关系,说也不是说得很清楚,下面直接上代码,通过代码来理解。

三. Talk is cheap, show you the code.

(1)首先,先来看主布局文件:activity_main.xml:

<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools" 
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true"
    tools:context="com.study.wnw.recyclerviewdivider.MainActivity">

    <android.support.v7.widget.RecyclerView
        android:id="@+id/recyclerview"
        android:layout_width="match_parent"
       android:layout_height="match_parent">    
    </android.support.v7.widget.RecyclerView>
</android.support.design.widget.CoordinatorLayout>

我在这里面仅仅加入了一个RecyclerView


(2)RecyclerView里面每个Item的布局文件:item_view.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:orientation="vertical"
              android:layout_width="match_parent" 
              android:layout_height="50dp"
              android:layout_margin="16sp">
    <TextView
        android:id="@+id/list_item"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center"
        android:textSize="20sp"
        android:textColor="#f7f4f7"
        android:background="#08da1d"/>
</LinearLayout>

这也没有什么可讲的,就是在里面添加一个TextView用来显示文本


(3)我们RecyclerView的适配器MyAdapater.java:

package com.study.wnw.recyclerviewdivider;
import android.content.Context;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
/** * Created by wnw on 16-5-22. */
public class MyAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {

    //定义一个集合,接收从Activity中传递过来的数据和上下文
    private List<String> mList;
    private Context mContext;

    MyAdapter(Context context, List<String> list){
        this.mContext = context;
        this.mList = list;
    }

    //得到child的数量
    @Override
    public int getItemCount() {
        return mList.size();
    }
    
    //创建ChildView
    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View layout = LayoutInflater.from(mContext).inflate(R.layout.item_view, parent, false);
        return new MyHolder(layout);
    }

    //将数据绑定到每一个childView中
    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
        if (holder instanceof MyHolder){
            final String itemText = mList.get(position);
            ((MyHolder)holder).tv.setText(itemText);
        }
    }

    // 通过holder的方式来初始化每一个ChildView的内容
    class MyHolder extends RecyclerView.ViewHolder{
        TextView tv;
        public MyHolder(View itemView) {
            super(itemView);
            tv = (TextView)itemView.findViewById(R.id.list_item);
        }
    }
}

好了,这里也没有什么好讲的,也不是我们这篇文章的重点,下面重点来了。


(4)我们自定义的MyDecoration.java:(继承RecyclerView.ItemDecoration)

package com.study.wnw.recyclerviewdivider;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.view.View;

/** * Created by wnw on 16-5-22. */

public class MyDecoration extends RecyclerView.ItemDecoration{

    private Context mContext;
    private Drawable mDivider;
    private int mOrientation;
    public static final int HORIZONTAL_LIST = LinearLayoutManager.HORIZONTAL;
    public static final int VERTICAL_LIST = LinearLayoutManager.VERTICAL;

    //我们通过获取系统属性中的listDivider来添加,在系统中的AppTheme中设置
    public static final int[] ATRRS  = new int[]{
            android.R.attr.listDivider
    };

    public MyDecoration(Context context, int orientation) {
        this.mContext = context;
        final TypedArray ta = context.obtainStyledAttributes(ATRRS);
        this.mDivider = ta.getDrawable(0);
        ta.recycle();
        setOrientation(orientation);
    }

    //设置屏幕的方向
    public void setOrientation(int orientation){
        if (orientation != HORIZONTAL_LIST && orientation != VERTICAL_LIST){
            throw new IllegalArgumentException("invalid orientation");        }        mOrientation = orientation;
    } 

   @Override
    public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
        if (mOrientation == HORIZONTAL_LIST){
            drawVerticalLine(c, parent, state);
        }else {
            drawHorizontalLine(c, parent, state);
        }
    }

    //画横线, 这里的parent其实是显示在屏幕显示的这部分
    public void drawHorizontalLine(Canvas c, RecyclerView parent, RecyclerView.State state){
        int left = parent.getPaddingLeft();
        int right = parent.getWidth() - parent.getPaddingRight();
        final int childCount = parent.getChildCount();
        for (int i = 0; i < childCount; i++){
            final View child = parent.getChildAt(i);

            //获得child的布局信息
            final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)child.getLayoutParams();
            final int top = child.getBottom() + params.bottomMargin;
            final int bottom = top + mDivider.getIntrinsicHeight();
            mDivider.setBounds(left, top, right, bottom);
            mDivider.draw(c);
            //Log.d("wnw", left + " " + top + " "+right+"   "+bottom+" "+i);
        }
    }

    //画竖线
    public void drawVerticalLine(Canvas c, RecyclerView parent, RecyclerView.State state){
        int top = parent.getPaddingTop();
        int bottom = parent.getHeight() - parent.getPaddingBottom();
        final int childCount = parent.getChildCount();
        for (int i = 0; i < childCount; i++){
            final View child = parent.getChildAt(i); 

           //获得child的布局信息
            final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)child.getLayoutParams();
            final int left = child.getRight() + params.rightMargin;
            final int right = left + mDivider.getIntrinsicWidth();
            mDivider.setBounds(left, top, right, bottom);
            mDivider.draw(c);
        }
    }

    //由于Divider也有长宽高,每一个Item需要向下或者向右偏移
    @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
        if(mOrientation == HORIZONTAL_LIST){
            //画横线,就是往下偏移一个分割线的高度
            outRect.set(0, 0, 0, mDivider.getIntrinsicHeight());
        }else {
            //画竖线,就是往右偏移一个分割线的宽度
            outRect.set(0, 0, mDivider.getIntrinsicWidth(), 0);
        }
    }
}

从上面的代码中,我们还通过系统属性来适应屏幕的横屏和竖屏,然后确定画横的,还是竖的Divider,其实在里面我们做了三件事,*件是:获取到系统中的listDivider, 我们就是通过它在主题中去设置的,下面第(6)小点看一下代码就知道了。第二件事:就是找到我们需要添加Divider的位置,从onDraw方法中去找到,并将Divider添加进去。第三个是:得到Item的偏移量。


(5)看看我们的MainActivity.java

package com.study.wnw.recyclerviewdivider;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import java.util.ArrayList;
import java.util.List;
public class MainActivity extends AppCompatActivity {
    //定义RecyclerView
    private RecyclerView mRecyclerView = null;

    //定义一个List集合,用于存放RecyclerView中的每一个数据
    private List<String> mData = null;

    //定义一个Adapter
    private MyAdapter mAdapter; 

   //定义一个LinearLayoutManager
    private LinearLayoutManager mLayoutManager;

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

        //RecyclerView三步曲+LayoutManager
        initView();
        initData();
        mAdapter = new MyAdapter(this,mData);
        mRecyclerView.setLayoutManager(mLayoutManager);
        mRecyclerView.setAdapter(mAdapter); 

        //这句就是添加我们自定义的分隔线
        mRecyclerView.addItemDecoration(new MyDecoration(this, MyDecoration.VERTICAL_LIST));
    }

    //初始化View
    private void initView(){
        mLayoutManager = new LinearLayoutManager(this);
        mRecyclerView = (RecyclerView)findViewById(R.id.recyclerview);
    }

    //初始化加载到RecyclerView中的数据, 我这里只是给每一个Item添加了String类型的数据
    private void initData(){
        mData = new ArrayList<String>();
        for (int i = 0; i < 20; i++){
            mData.add("Item" + i);
        }
    }
}
(6)分隔线Divider的drawable文件:divider..xml

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle">
    <solid android:color="#7b7a7a"/>
    <size android:height="1dp"/>
</shape>

我们在这里面,画了一个:rectangle, 给它填充颜色,还有高度,这样就搞定了,高度小,显示出来也是一条线:其实线的本质就是长方形。这里可以根据个人需要,画不同类型的divider


(7)在styles.xml的AppTheme中,设置listDivider为我们的divider.xml文件:

<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
    <item name="android:listDivider">@drawable/divider</item>
</style>

这样,我们将系统的listDivider设置成我们自定义的divider. 还记得我们在MyDecoration中获取系统的listDivider这个属性吗,这样通过这个属性,我们就可以将我们的divider.xml文件和MyDecoration.java进行关联了。


到这里所有的工作就完成了,下面展示一下运行结果:

%title插图%num
竖屏效果图
%title插图%num
横屏效果图

经过几个小时的写作,终于搞定了,虽然仅仅是一个添加分隔线的功能,但是还是想尽可能的通过自己的语言去理解,去认知它的原理,这样做起来就简单多了。一开始的时候,我夜不知道怎么去用,也参考了别人写的文章,特别是鸿洋大神的:Android RecyclerView 使用完全解析 体验艺术般的控件, 写得特别的棒,从中也学到了一些知识。


好了,这篇文章暂时写到这里了,简单的介绍了一些RecyclerView分隔线的原理和添加方法,希望大家能够多多交流,过几天我会继续写下一篇文章,RecyclerView系列之(3):为RecyclerView添加下拉刷新和上拉加载的功能。*后还是要感谢大家,感谢这个平台,能够让我们一起交流,一切学习。

 

为RecyclerView添加Header和Footer

  • 过去的两天,在项目中,抛弃了ListView, 想试一试RecyclerView, 在用的过程中,遇到了一些问题,比如:如何为RecyclerView添加Header和Footer? 如何为RecyclerView添加分割线?如何为RecyclerView添加下拉刷新和上拉加载? 在今后的一段时间里,我会针对这几个问题,通过写简书的方式一一讲述, 今天为大家带来的是*个问题的解决方法,如何为RecyclerView添加Header和Footer?在这之前,我想分享一下我对RecyclerView的认识:

一 . 我眼中的RecyclerView

  • 过去的这一两年, RecyclerView的越来越引起了我们Android开发人员的注意,甚至很多人都说:ListView, GridView已经逐渐被RecyclerView替代, *主要的原因就是RecyclerView的灵活性, 还有性能上的提升。那么也许有很多人会问:RecyclerView和ListView, GridView到底是什么关系呢?通过Android官方文档的一组截图告诉你:
    %title插图%num
    ListView的家族谱(继承结构)
%title插图%num
RecyclerView的家族谱(继承结构)

* RecyclerView的家族谱(继承结构)*

  • 通过上面的两个图可以发现, ListView继承自:AbsListView。(GirdView也是), RecyclerView直接继承了ViewGroup , *后得出结论:RecyclerView是ListView的爷爷辈, 也就是RecyclerView是ListView的二爷, 所以从封装的层次上得出了为什么RecyclerView性能比ListView更好的原因, 因为封装的层次越高,查询执行的速度相对较慢,当然还有一个原因,RecyclerView中自带的Adapter中加入了Holder,强制要求开发人员使用,在ListView中,很多人都不懂使用Holder, 这也导致了使用ListView性能比较差。这也是RecyclerView性能提升的一个主要原因, 当然,封装的层越高越灵活,相对使用起来也相对难,很多方法都是通过自己去封装的,比如ListView中有addHeaderView(View view), addFooterView(View view)去添加自己的Header 和Footer, 但是在RecyclerView中没有,下面我将详细介绍:

二 . 为RecyclerView添加Header和Footer

1. 认识Header和Footer

通过下面简书的两张图,让你认识Header和Footer

%title插图%num
带Header的图

带Header的图

%title插图%num
带Footer的图

带Footer的图

  • 通过上面的两张图,可以看到,Header和Footer很多情况下是辅助功能的,比如Header通常用于做广告,而Footer更多的是为了显示下拉加载更多作为一个可视化组件来显示加载情况,提升用户体验。

2. 通过代码来解析如何为RecyclerView添加Header和Footer

RecyclerView的使用和ListView的使用差不多,无非就是那三步: *,初始化RecyclerView; 第二,初始化数据,并且将数据通过Adapter装在到View中; 第三,通过setAdapter方法,将Adapter绑定到RecyclerView中。 下面直接上代码:通过代码注释的方式来讲解添加Header和Footer: (1) 主布局layout_main.xml: 只是添加了一个RecyclerView而已

<pre class=”hljs undefined” data-original-code=”” “=”” data-snippet-id=”ext.035ab4069719aee80b16158b5db41e1e” data-snippet-saved=”false” data-codota-status=”done”>

  1. <?xml version=”1.0″ encoding=”utf-8″?>
  2. <RelativeLayoutxmlns:android=”http://schemas.android.com/apk/res/android
  3. xmlns:tools=“http://schemas.android.com/tools”
  4. android:layout_width=“match_parent”
  5. android:layout_height=“match_parent”
  6. android:paddingBottom=“@dimen/activity_vertical_margin”
  7. android:paddingLeft=“@dimen/activity_horizontal_margin”
  8. android:paddingRight=“@dimen/activity_horizontal_margin”
  9. android:paddingTop=“@dimen/activity_vertical_margin”
  10. tools:context=“com.study.wnw.recyclerviewheaderfooter.MainActivity”>
  11. <android.support.v7.widget.RecyclerView
  12. android:id=“@+id/recyclerview”
  13. android:layout_width=“match_parent”
  14. android:layout_height=“match_parent”/>
  15. </RelativeLayout>

(2)RecyclerView中ListView的Item布局list_Item.xml: 只是添加了一个TextView

  1. <?xml version=”1.0″ encoding=”utf-8″?><LinearLayout
  2. xmlns:android=“http://schemas.android.com/apk/res/android”
  3. android:orientation=“vertical”
  4. android:layout_width=“match_parent”
  5. android:layout_height=“50dp”>
  6. <TextView
  7. android:id=“@+id/item”
  8. android:layout_width=“match_parent”
  9. android:layout_height=“match_parent”
  10. android:textSize=“20sp”
  11. android:textColor=“@color/colorAccent”
  12. android:gravity=“center”
  13. android:background=“#08e630”/>
  14. </LinearLayout>

(3)HeaderView和FooterView的布局文件,也是一个TextView, 这里只贴出了HeaderView的布局,FooterView的布局文件和FooterView的一样:

HeaderView的布局文件: header.xml:

  1. <?xml version=”1.0″ encoding=”utf-8″?>
  2. <LinearLayout xmlns:android=“http://schemas.android.com/apk/res/android”
  3. android:orientation=“vertical”
  4. android:layout_width=“match_parent”
  5. android:layout_height=“100dp”>
  6. <TextView
  7. android:id=“@+id/header”
  8. android:layout_width=“match_parent”
  9. android:layout_height=“match_parent”
  10. android:text=“我是Header”
  11. android:textSize=“30sp”
  12. android:textColor=“#fde70b0b”
  13. android:background=“#f9777979”
  14. android:gravity=“center”/>
  15. </LinearLayout>

好了, 布局文件到这里我们已经弄好了, 下面我们直接看MainActivity中的内容:
(4) MainActivity.java中的内容为:

  1. package com.study.wnw.recyclerviewheaderfooter;
  2. import android.app.Activity;import android.os.Bundle;
  3. import android.support.v7.widget.LinearLayoutManager;
  4. import android.support.v7.widget.RecyclerView;
  5. import android.view.LayoutInflater;
  6. import android.view.View;
  7. import java.util.ArrayList;import java.util.List;
  8. public class MainActivity extends Activity {
  9. private RecyclerView mRecyclerView;
  10. private MyAdapter mMyAdapter;
  11. private List<String> mList;
  12. @Override
  13. protected void onCreate(Bundle savedInstanceState) {
  14. super.onCreate(savedInstanceState);
  15. setContentView(R.layout.activity_main);
  16. //RecyclerView三部曲+LayoutManager
  17. mRecyclerView = (RecyclerView)findViewById(R.id.recyclerview);
  18. LinearLayoutManager linearLayoutManager = new LinearLayoutManager(this);
  19. mRecyclerView.setLayoutManager(linearLayoutManager);
  20. initData();
  21. mMyAdapter = new MyAdapter(mList);
  22. mRecyclerView.setAdapter(mMyAdapter);
  23. //为RecyclerView添加HeaderView和FooterView
  24. setHeaderView(mRecyclerView);
  25. setFooterView(mRecyclerView);
  26. }
  27. //初始化RecyclerView中每个item的数据
  28. private void initData(){
  29. mList = new ArrayList<String>();
  30. for (int i = 0; i < 20; i++){
  31. mList.add(“item” + i);
  32. }
  33. }
  34. private void setHeaderView(RecyclerView view){
  35. View header = LayoutInflater.from(this).inflate(R.layout.header, view, false);
  36. mMyAdapter.setHeaderView(header);
  37. }
  38. private void setFooterView(RecyclerView view){
  39. View footer = LayoutInflater.from(this).inflate(R.layout.footer, view, false);
  40. mMyAdapter.setFooterView(footer);
  41. }
  42. }
  • 从上面的代码中,我们可以看到,我们在MainActivity中做了两件事,一个是初始化RecyclerView相关的View, Adapter, data; 另一个是通过我们自定义的Adapter的setHeaderView()和setFooterView()方法为RecyclerView添加HeaderView和FooterView, 到这里,我们已经迫不及待的想知道MyAdapter中到底有什么东西, 直接上代码
    (5) MyAdapter.java的代码
  1. package com.study.wnw.recyclerviewheaderfooter;
  2. import android.support.v7.widget.RecyclerView;
  3. import android.view.LayoutInflater;import android.view.View;
  4. import android.view.ViewGroup;
  5. import android.widget.TextView;
  6. import java.util.List;
  7. /** * Created by wnw on 16-5-20. */
  8. public class MyAdapter extendsRecyclerView.Adapter<RecyclerView.ViewHolder> {
  9. public static final int TYPE_HEADER = 0; //说明是带有Header的
  10. public static final int TYPE_FOOTER = 1; //说明是带有Footer的
  11. public static final int TYPE_NORMAL = 2; //说明是不带有header和footer的
  12. //获取从Activity中传递过来每个item的数据集合
  13. private List<String> mDatas;
  14. //HeaderView, FooterView
  15. private View mHeaderView;
  16. private View mFooterView;
  17. //构造函数
  18. public MyAdapter(List<String> list){
  19. this.mDatas = list;
  20. }
  21. //HeaderView和FooterView的get和set函数
  22. public View getHeaderView() {
  23. return mHeaderView;
  24. }
  25. public void setHeaderView(View headerView) {
  26. mHeaderView = headerView;
  27. notifyItemInserted(0);
  28. }
  29. public View getFooterView() {
  30. return mFooterView;
  31. }
  32. public void setFooterView(View footerView) {
  33. mFooterView = footerView;
  34. notifyItemInserted(getItemCount()-1);
  35. }
  36. /** 重写这个方法,很重要,是加入Header和Footer的关键,我们通过判断item的类型,从而绑定不同的view * */
  37. @Override
  38. public int getItemViewType(int position) {
  39. if (mHeaderView == null && mFooterView == null){
  40. return TYPE_NORMAL;
  41. }
  42. if (position == 0){
  43. //*个item应该加载Header
  44. return TYPE_HEADER;
  45. }
  46. if (position == getItemCount()-1){
  47. //*后一个,应该加载Footer
  48. return TYPE_FOOTER;
  49. }
  50. return TYPE_NORMAL;
  51. }
  52. //创建View,如果是HeaderView或者是FooterView,直接在Holder中返回
  53. @Override
  54. public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
  55. if(mHeaderView != null && viewType == TYPE_HEADER) {
  56. return new ListHolder(mHeaderView);
  57. }
  58. if(mFooterView != null && viewType == TYPE_FOOTER){
  59. return new ListHolder(mFooterView);
  60. }
  61. View layout = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item, parent, false);
  62. return new ListHolder(layout);
  63. }
  64. //绑定View,这里是根据返回的这个position的类型,从而进行绑定的, HeaderView和FooterView, 就不同绑定了
  65. @Override
  66. public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
  67. if(getItemViewType(position) == TYPE_NORMAL){
  68. if(holder instanceof ListHolder) {
  69. //这里加载数据的时候要注意,是从position-1开始,因为position==0已经被header占用了
  70. ((ListHolder) holder).tv.setText(mDatas.get(position-1));
  71. return;
  72. }
  73. return;
  74. }else if(getItemViewType(position) == TYPE_HEADER){
  75. return;
  76. }else{
  77. return;
  78. }
  79. }
  80. //在这里面加载ListView中的每个item的布局
  81. class ListHolder extends RecyclerView.ViewHolder{
  82. TextView tv;
  83. public ListHolder(View itemView) {
  84. super(itemView);
  85. //如果是headerview或者是footerview,直接返回
  86. if (itemView == mHeaderView){
  87. return;
  88. }
  89. if (itemView == mFooterView){
  90. return;
  91. }
  92. tv = (TextView)itemView.findViewById(R.id.item);
  93. }
  94. }
  95. //返回View中Item的个数,这个时候,总的个数应该是ListView中Item的个数加上HeaderView和FooterView
  96. @Override
  97. public int getItemCount() {
  98. if(mHeaderView == null && mFooterView == null){
  99. return mDatas.size();
  100. }else if(mHeaderView == null && mFooterView != null){
  101. return mDatas.size() + 1;
  102. }else if (mHeaderView != null && mFooterView == null){
  103. return mDatas.size() + 1;
  104. }else {
  105. return mDatas.size() + 2;
  106. }
  107. }
  108. }
  • 从上面的MyAdapter类中,有setHeaderView()和setFooterView()两个方法,我们就是通过这两个方法从Activity将headerView和footerView传递过来的, 在Adapter中的onCreateViewHolder()方法中,利用getItemViewType()返回Item的类型(你这个Item是不是Header家的?还是Footer家的?或者是ListView家的?)根据不同的类型,我们创建不同的Item的View。大概的思路就是这样子,更多细节请看代码注释,下面是运行截图(由于不知道Linux下有什么好的GIF生成工具,只能截静态图,大家知道的话,可以推荐给我,谢谢各位大神):
%title插图%num
header
%title插图%num
footer
  • 终于写完了,需要慢慢消化和吸收,这只是一种实现HeaderView和FooterView的方式,我们从这里看到,HeaderView和FooterView是直接从Activity中传递过Adapter的,我们也可以像ListView一样,我们只传递HeaderView和FooterView的数据过Adapter中,然后在Adapter的内部再新建不同的HeaderHolder和FooterHolder, 根据返回Item的类型,加载不同的Holder即可,具体的实现我就不多说了,大家可以去实现一下。通过截图,你会发现,RecyclerView中,每个Item之间竟然没有分割线,而且RecyclerView中也没有setDivider()这样的方法去设置分割线,因为RecyclerView灵活啊,所以也是要自定义的,其实很简单,下一篇文章我会带来如何为RecyclerView添加分割线,这篇就到此结束了,有不足之处,望大家多多指教,谢谢了。
    RecyclerView中添加分隔线已经实现

Android端实现多人音视频聊天应用(二)

本篇主要讨论如何使用 Agora SDK 进行多人聊天。主要需要实现以下功能:

  1. 上一篇已经实现过的聊天功能
  2. 随着加入人数和他们的手机摄像头分辨率的变化,显示不同的UI,即所谓的“分屏”
  3. 点击分屏中的小窗,可以放大显示该聊天窗

分屏

根据前期技术调研,分屏显示*好的方式是采用瀑布流结合动态聊天窗实现,这样比较方便的能够适应UI的变化。所谓瀑布流,就是目前比较流行的一种列表布局,会在界面上呈现参差不齐的多栏布局。我们先实现一个瀑布流:

瀑布流的实现方式很多,本文采用结合 GridLayoutManager的RecyclerView 来实现。我们首先自定义一个 RecyclerView,命名为 GridVideoViewContainer。核心代码如下:

  1. int count = uids.size();
  2. if (count <= 2) {
  3. // 只有本地视频或聊天室内只有另外一个人
  4. this.setLayoutManager(new LinearLayoutManager(activity.getApplicationContext(), orientation, false));
  5. } else if (count > 2) {
  6. // 多人聊天室
  7. int itemSpanCount = getNearestSqrt(count);
  8. this.setLayoutManager(new GridLayoutManager(activity.getApplicationContext(), itemSpanCount, orientation, false));
  9. }
  10. 复制代码

根据上面的代码可以看出,在聊天室里只有自己的本地视频或者只有另外一个人的时候,采用 LinearLayoutManager,这样的布局其实与前文的一对一聊天类似;而在真正意义的多人聊天室里,则采用 GridLayoutManager 实现瀑布流,其中 itemSpanCount 就是瀑布流的列数。

有了一个可用的瀑布流之后,下面我们就可以实现动态聊天窗了: 动态聊天窗的要点在于 item 的大小由视频的宽高比决定,因此 Adapter 及其对应的 layout 就该注意不要写死尺寸。在 Adapter 里控制 item 具体尺寸的代码如下:

  1. if (force || mItemWidth == 0 || mItemHeight == 0) {
  2. WindowManager windowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
  3. DisplayMetrics outMetrics = new DisplayMetrics();
  4. windowManager.getDefaultDisplay().getMetrics(outMetrics);
  5. int count = uids.size();
  6. int DividerX = 1;
  7. int DividerY = 1;
  8. if (count == 2) {
  9. DividerY = 2;
  10. } else if (count >= 3) {
  11. DividerX = getNearestSqrt(count);
  12. DividerY = (int) Math.ceil(count * 1.f / DividerX);
  13. }
  14. int width = outMetrics.widthPixels;
  15. int height = outMetrics.heightPixels;
  16. if (width > height) {
  17. mItemWidth = width / DividerY;
  18. mItemHeight = height / DividerX;
  19. } else {
  20. mItemWidth = width / DividerX;
  21. mItemHeight = height / DividerY;
  22. }
  23. }
  24. 复制代码

以上代码根据视频的数量确定了列数和行数,然后根据列数和屏幕宽度确定了视频的宽度,接着根据视频的宽高比和视频宽度确定了视频高度。同时也考虑了手机的横竖屏情况(就是if (width > height)这行代码)。

该 Adapter 对应的 layout 的代码如下:

  1. <RelativeLayout
  2. xmlns:android=“http://schemas.android.com/apk/res/android”
  3. android:id=“@+id/user_control_mask”
  4. android:layout_width=“match_parent”
  5. android:layout_height=“match_parent”
  6. android:orientation=“vertical”>
  7. <ImageView
  8. android:id=“@+id/default_avatar”
  9. android:layout_width=“wrap_content”
  10. android:layout_height=“wrap_content”
  11. android:layout_centerInParent=“true”
  12. android:visibility=“gone”
  13. android:src=“@drawable/icon_default_avatar”
  14. android:contentDescription=“DEFAULT_AVATAR” />
  15. <ImageView
  16. android:id=“@+id/indicator”
  17. android:layout_width=“wrap_content”
  18. android:layout_height=“wrap_content”
  19. android:layout_centerHorizontal=“true”
  20. android:layout_alignParentBottom=“true”
  21. android:layout_marginBottom=“@dimen/video_indicator_bottom_margin”
  22. android:contentDescription=“VIDEO_INDICATOR” />
  23. <LinearLayout
  24. android:id=“@+id/video_info_container”
  25. android:layout_width=“wrap_content”
  26. android:layout_height=“wrap_content”
  27. android:layout_alignParentTop=“true”
  28. android:layout_marginTop=“24dp”
  29. android:layout_marginStart=“15dp”
  30. android:layout_marginLeft=“15dp”
  31. android:visibility=“gone”
  32. android:orientation=“vertical”>
  33. <TextView
  34. android:id=“@+id/video_info_metadata”
  35. android:layout_width=“wrap_content”
  36. android:layout_height=“wrap_content”
  37. android:singleLine=“true”
  38. style=“@style/NotificationUIText” />
  39. </LinearLayout>
  40. </RelativeLayout>
  41. 复制代码

我们可以看到,layout 中有关尺寸的属性都 是wrap_content,这就使得 item 大小随视频宽高比变化成为可能。

把分屏的布局写好之后,我们就可以在每一个 item 上播放聊天视频了。

播放聊天视频

在 Agora SDK 中一个远程视频的显示只和该用户的 UID 有关,所以使用的数据源只需要简单定义为包含 UID 和对应的 SurfaceView 即可,就像这样:

  1. private final HashMap<Integer, SurfaceView> mUidsList = new HashMap<>();
  2. 复制代码

每当有人加入了我们的聊天频道,都会触发onFirstRemoteVideoDecoded(int uid, int width, int height, int elapsed)方法,*个 uid 就是他们的 UID;接下来我们要为每个 item 新建一个 SurfaceView 并为其创建渲染视图,*后将它们加入刚才创建好的mUidsList里并调用setupRemoteVideo( VideoCanvas remote )方法播放这个聊天视频。这个过程的完整代码如下:

  1. @Override
  2. public void onFirstRemoteVideoDecoded(int uid, int width, int height, int elapsed) {
  3. doRenderRemoteUi(uid);
  4. }
  5. private void doRenderRemoteUi(final int uid) {
  6. runOnUiThread(new Runnable() {
  7. @Override
  8. public void run() {
  9. if (isFinishing()) {
  10. return;
  11. }
  12. if (mUidsList.containsKey(uid)) {
  13. return;
  14. }
  15. SurfaceView surfaceV = RtcEngine.CreateRendererView(getApplicationContext());
  16. mUidsList.put(uid, surfaceV);
  17. boolean useDefaultLayout = mLayoutType == LAYOUT_TYPE_DEFAULT;
  18. surfaceV.setZOrderOnTop(true);
  19. surfaceV.setZOrderMediaOverlay(true);
  20. rtcEngine().setupRemoteVideo(new VideoCanvas(surfaceV, VideoCanvas.RENDER_MODE_HIDDEN, uid));
  21. if (useDefaultLayout) {
  22. log.debug(“doRenderRemoteUi LAYOUT_TYPE_DEFAULT “ + (uid & 0xFFFFFFFFL));
  23. switchToDefaultVideoView();
  24. } else {
  25. int bigBgUid = mSmallVideoViewAdapter == null ? uid : mSmallVideoViewAdapter.getExceptedUid();
  26. log.debug(“doRenderRemoteUi LAYOUT_TYPE_SMALL “ + (uid & 0xFFFFFFFFL) + ” “ + (bigBgUid & 0xFFFFFFFFL));
  27. switchToSmallVideoView(bigBgUid);
  28. }
  29. }
  30. });
  31. }
  32. 复制代码

以上代码与前文中播放一对一视频的代码如出一撤,但是细心的读者可能已经发现我们并没有将生成的 SurfaceView 放在界面里,这正是与一对一视频的不同之处:我们要在一个抽象的 VideoViewAdapter 类里将 SurfaceView 放出来,关键代码如下:

  1. SurfaceView target = user.mView;
  2. VideoViewAdapterUtil.stripView(target);
  3. holderView.addView(target, 0, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
  4. 复制代码

一般 Android 工程师看见 holderView 就明白这是 ViewHolder 的 layout 的根 layout 了,而 user 是哪儿来的,详见文末的代码,文中不做赘述。

这样在多人聊天的时候我们就能使用分屏的方式播放用户聊天视频了,如果想放大某一个用户的视频该怎么办呢?

全屏和小窗

当用户双击某一个 item 的时候,他希望对应的视频能够全屏显示,而其他的视频则变成小窗口,那么我们先定义一个双击事件接口:

  1. public interface VideoViewEventListener {
  2. void onItemDoubleClick(View v, Object item);
  3. }
  4. 具体实现方式如下:
  5. mGridVideoViewContainer.setItemEventHandler(new VideoViewEventListener() {
  6. @Override
  7. public void onItemDoubleClick(View v, Object item) {
  8. log.debug(“onItemDoubleClick “ + v + ” “ + item + ” “ + mLayoutType);
  9. if (mUidsList.size() < 2) {
  10. return;
  11. }
  12. UserStatusData user = (UserStatusData) item;
  13. int uid = (user.mUid == 0) ? config().mUid : user.mUid;
  14. if (mLayoutType == LAYOUT_TYPE_DEFAULT && mUidsList.size() != 1) {
  15. switchToSmallVideoView(uid);
  16. } else {
  17. switchToDefaultVideoView();
  18. }
  19. }
  20. });
  21. 复制代码

将被选中的视频全屏播放的方法很容易理解,我们只看生成小窗列表的方法:

  1. private void switchToSmallVideoView(int bigBgUid) {
  2. HashMap<Integer, SurfaceView> slice = new HashMap<>(1);
  3. slice.put(bigBgUid, mUidsList.get(bigBgUid));
  4. Iterator<SurfaceView> iterator = mUidsList.values().iterator();
  5. while (iterator.hasNext()) {
  6. SurfaceView s = iterator.next();
  7. s.setZOrderOnTop(true);
  8. s.setZOrderMediaOverlay(true);
  9. }
  10. mUidsList.get(bigBgUid).setZOrderOnTop(false);
  11. mUidsList.get(bigBgUid).setZOrderMediaOverlay(false);
  12. mGridVideoViewContainer.initViewContainer(this, bigBgUid, slice, mIsLandscape);
  13. bindToSmallVideoView(bigBgUid);
  14. mLayoutType = LAYOUT_TYPE_SMALL;
  15. requestRemoteStreamType(mUidsList.size());
  16. }
  17. 复制代码

小窗列表要注意移除全屏的那个 UID,此外一切都和正常瀑布流视图相同,包括双击小窗的item将其全屏播放。

到了这里我们就已经使用 Agora SDK 完成了一个有基本功能的简单多人聊天 demo,要产品化还有很多的东西要做,在这里先做一个简单的总结吧!

总结

声网Agora 提供了高质量的视频通信 SDK,不仅覆盖了主流的操作系统,集成效率也比较高,而且还支持包括聊天,会议,直播等功能在内的多个模式的视频通话。SDK 中 API 设计基本能够满足大部分的开发需要,而且隐藏了底层开发,只需要提供 SurfaceView 和 UID 即可播放视频,这样对于 App 层的开发者来说十分友好。非常适合有视频聊天开发需求的开发者。在视频领域创业大爆发的今天,建议更多的想要从事该领域的开发者可以尝试下。

相关资源:AndroidAndroid屏幕共享共享你的屏幕和音频到另一台手机
友情链接: SITEMAP | 旋风加速器官网 | 旋风软件中心 | textarea | 黑洞加速器 | jiaohess | 老王加速器 | 烧饼哥加速器 | 小蓝鸟 | tiktok加速器 | 旋风加速度器 | 旋风加速 | quickq加速器 | 飞驰加速器 | 飞鸟加速器 | 狗急加速器 | hammer加速器 | trafficace | 原子加速器 | 葫芦加速器 | 麦旋风 | 油管加速器 | anycastly | INS加速器 | INS加速器免费版 | 免费vqn加速外网 | 旋风加速器 | 快橙加速器 | 啊哈加速器 | 迷雾通 | 优途加速器 | 海外播 | 坚果加速器 | 海外vqn加速 | 蘑菇加速器 | 毛豆加速器 | 接码平台 | 接码S | 西柚加速器 | 快柠檬加速器 | 黑洞加速 | falemon | 快橙加速器 | anycast加速器 | ibaidu | moneytreeblog | 坚果加速器 | 派币加速器 | 飞鸟加速器 | 毛豆APP | PIKPAK | 安卓vqn免费 | 一元机场加速器 | 一元机场 | 老王加速器 | 黑洞加速器 | 白石山 | 小牛加速器 | 黑洞加速 | 迷雾通官网 | 迷雾通 | 迷雾通加速器 | 十大免费加速神器 | 猎豹加速器 | 蚂蚁加速器 | 坚果加速器 | 黑洞加速 | 银河加速器 | 猎豹加速器 | 海鸥加速器 | 芒果加速器 | 小牛加速器 | 极光加速器 | 黑洞加速 | movabletype中文网 | 猎豹加速器官网 | 烧饼哥加速器官网 | 旋风加速器度器 | 哔咔漫画 | PicACG | 雷霆加速