这周的工作主要是做用户的开户界面。使用了fragment和第三方的自定义stepview组合完成,加上了fragment的切换动画,效果类似于按钮点击切换的viewpager。
1. 类似于微信好友列表按首字母定位自定义view实现并解决RecycleView 的不能准确定位的问题
记录一下主要流程,首先是自定义view的初始化,在这个回调方法中获取测量后的宽度和每一小格的高度,这里说一下这个方法的调用时机是在onMeasured之后,调用顺序是 onFinishInflate > onMeasure > onSizeChanged > onDraw。
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
width = getMeasuredWidth();
latticeHeight = getMeasuredHeight() * 1f / indexArr.length;
}
绘制方法很简单。
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
for (int i = 0; i < indexArr.length; i++) {
float x = width / 2;
float y = latticeHeight / 2 + getTextHeight(indexArr[i]) / 2 + i * latticeHeight;
paint.setColor(lastIndex == i ? getResources().getColor(R.color.colorPrimaryDark) : Color.GRAY);
Circlepaint.setColor(lastIndex == i ? getResources().getColor(R.color.colorPrimary) : Color.WHITE);
canvas.drawCircle(x, y - getTextHeight(indexArr[i]) / 2, 13.0f, Circlepaint);
canvas.drawText(indexArr[i], x, y, paint);
}
}
注意一下这个方法,获取当前text高度。
private int getTextHeight(String text) {
Rect rect = new Rect();
paint.getTextBounds(text, 0, text.length(), rect);
return rect.height();
}
事件处理方法,当当前触摸的地方不是和原来的lastIndex一致就去刷新重新绘制,并且做出回调接口,传递的是当前选择的字母。
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_MOVE:
float y = event.getY();
int index = (int) (y / latticeHeight);
if (lastIndex != index) {
if (index >= 0 && index < indexArr.length) {
if (listener != null) {
listener.onTouchIndex(indexArr[index]);
}
}
}
lastIndex = index;
break;
case MotionEvent.ACTION_UP:
lastIndex = -1;
break;
}
invalidate();
return true;
}
接下来解决的是一个RecyclerView无法精确滑动的问题,有一下几种情况
private void smoothMoveToPosition(final int position) {
int firstItem = personManageRecyclerview.getChildLayoutPosition(personManageRecyclerview.getChildAt(0));
int lastItem = personManageRecyclerview.getChildLayoutPosition(personManageRecyclerview.getChildAt(personManageRecyclerview.getChildCount() -1));
if (position < firstItem ) {
// 如果要跳转的位置在第一个可见项之前,则smoothScrollToPosition可以直接跳转
personManageRecyclerview.smoothScrollToPosition(position);
} else if (position <= lastItem) {
// 如果要跳转的位置在第一个可见项之后,且在最后一个可见项之前
// smoothScrollToPosition根本不会动,此时调用smoothScrollBy来滑动到指定位置
int movePosition = position - firstItem;
if (movePosition >= 0 && movePosition < personManageRecyclerview.getChildCount()) {
int top = personManageRecyclerview.getChildAt(movePosition).getTop();
personManageRecyclerview.smoothScrollBy(0, top);
}
} else {
// 如果要跳转的位置在最后可见项之后,则先调用smoothScrollToPosition将要跳转的位置滚动到可见位置
// 再通过onScrollStateChanged控制再次调用smoothMoveToPosition,进入上一个控制语句
personManageRecyclerview.smoothScrollToPosition(position);
mShouldScroll = true;
mToPosition = position;
}
}
第三种情况要递归一次方法
@Override
public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
if (newState == RecyclerView.SCROLL_STATE_IDLE) {
if (mShouldScroll) {
mShouldScroll = false;
smoothMoveToPosition(mToPosition);
}
}
}
2. 实现自定义RecyclerView的侧滑删除
与listView中的getFirstVisiblePosition()方法一致,都是获取当前界面的显示出来的第一个item,在所有itemlist中的下标。
int firstPosition = ((LinearLayoutManager) getLayoutManager()).findFirstVisibleItemPosition();
获取当前的已经可见的item数量
final int count = getChildCount();
代码
public class SlideRecyclerView extends RecyclerView {
private static final String TAG = "SlideRecyclerView";
private static final int INVALID_POSITION = -1; // 触摸到的点不在子View范围内
private static final int INVALID_CHILD_WIDTH = -1; // 子ItemView不含两个子View
private static final int SNAP_VELOCITY = 600; // 最小滑动速度
private VelocityTracker mVelocityTracker; // 速度追踪器
private int mTouchSlop; // 认为是滑动的最小距离(一般由系统提供)
private Rect mTouchFrame; // 子View所在的矩形范围
private Scroller mScroller;
private float mLastX; // 滑动过程中记录上次触碰点X
private float mFirstX, mFirstY; // 首次触碰范围
private boolean mIsSlide; // 是否滑动子View
private ViewGroup mFlingView; // 触碰的子View
private int mPosition; // 触碰的view的位置
private int mMenuViewWidth; // 菜单按钮宽度
public SlideRecyclerView(@NonNull Context context) {
this(context, null);
}
public SlideRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public SlideRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
mScroller = new Scroller(context);
}
/**
* descirption: 外部使用了ViewPager所以做请求不要拦截
*/
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
getParent().requestDisallowInterceptTouchEvent(true);
return super.dispatchTouchEvent(ev);
}
/**
* descirption: 在按下事件中初始化位置和view,在拖动事件中按要求拦截事件
*/
@Override
public boolean onInterceptTouchEvent(MotionEvent e) {
int x = (int) e.getX();
int y = (int) e.getY();
obtainVelocity(e);
switch (e.getAction()) {
case MotionEvent.ACTION_DOWN:
if (!mScroller.isFinished()) { // 如果动画还没停止,则立即终止动画
mScroller.abortAnimation();
}
mFirstX = mLastX = x;
mFirstY = y;
mPosition = pointToPosition(x, y); // 获取触碰点所在的position
if (mPosition != INVALID_POSITION) {
View view = mFlingView;
// 获取触碰点所在的view
mFlingView = (ViewGroup) getChildAt(mPosition - ((LinearLayoutManager) getLayoutManager()).findFirstVisibleItemPosition());
// 这里判断一下如果之前触碰的view已经打开,而当前碰到的view不是那个view则立即关闭之前的view,此处并不需要担动画没完成冲突,因为之前已经abortAnimation
if (view != null && mFlingView != view && view.getScrollX() != 0) {
view.scrollTo(0, 0);
}
// 这里进行了强制的要求,RecyclerView的子ViewGroup必须要有2个子view,这样菜单按钮才会有值,
// 需要注意的是:如果不定制RecyclerView的子View,则要求子View必须要有固定的width。
// 比如使用LinearLayout作为根布局,而content部分width已经是match_parent,此时如果菜单view用的是wrap_content,menu的宽度就会为0。
if (mFlingView.getChildCount() == 2) {
mMenuViewWidth = mFlingView.getChildAt(1).getWidth();
} else {
mMenuViewWidth = INVALID_CHILD_WIDTH;
}
}
break;
case MotionEvent.ACTION_MOVE:
mVelocityTracker.computeCurrentVelocity(1000);
// 此处有俩判断,满足其一则认为是侧滑:
// 1.如果x方向速度大于y方向速度,且大于最小速度限制;
// 2.如果x方向的侧滑距离大于y方向滑动距离,且x方向达到最小滑动距离;
float xVelocity = mVelocityTracker.getXVelocity();
float yVelocity = mVelocityTracker.getYVelocity();
if (Math.abs(xVelocity) > SNAP_VELOCITY && Math.abs(xVelocity) > Math.abs(yVelocity)
|| Math.abs(x - mFirstX) >= mTouchSlop
&& Math.abs(x - mFirstX) > Math.abs(y - mFirstY)) {
mIsSlide = true;
return true;
}
break;
case MotionEvent.ACTION_UP:
releaseVelocity();
break;
}
return super.onInterceptTouchEvent(e);
}
@Override
public boolean onTouchEvent(MotionEvent e) {
if (mIsSlide && mPosition != INVALID_POSITION) {
float x = e.getX();
obtainVelocity(e);
switch (e.getAction()) {
case MotionEvent.ACTION_DOWN: // 因为没有拦截,所以不会被调用到
break;
case MotionEvent.ACTION_MOVE:
// 随手指滑动
if (mMenuViewWidth != INVALID_CHILD_WIDTH) {
float dx = mLastX - x;
if (mFlingView.getScrollX() + dx <= mMenuViewWidth
&& mFlingView.getScrollX() + dx > 0) {
mFlingView.scrollBy((int) dx, 0);
}
mLastX = x;
}
break;
case MotionEvent.ACTION_UP:
if (mMenuViewWidth != INVALID_CHILD_WIDTH) {
int scrollX = mFlingView.getScrollX();
mVelocityTracker.computeCurrentVelocity(1000);
// 此处有两个原因决定是否打开菜单:
// 1.菜单被拉出宽度大于菜单宽度一半;
// 2.横向滑动速度大于最小滑动速度;
// 注意:之所以要小于负值,是因为向左滑则速度为负值
if (mVelocityTracker.getXVelocity() < -SNAP_VELOCITY) { // 向左侧滑达到侧滑最低速度,则打开
mScroller.startScroll(scrollX, 0, mMenuViewWidth - scrollX, 0, Math.abs(mMenuViewWidth - scrollX));
} else if (mVelocityTracker.getXVelocity() >= SNAP_VELOCITY) { // 向右侧滑达到侧滑最低速度,则关闭
mScroller.startScroll(scrollX, 0, -scrollX, 0, Math.abs(scrollX));
} else if (scrollX >= mMenuViewWidth / 2) { // 如果超过删除按钮一半,则打开
mScroller.startScroll(scrollX, 0, mMenuViewWidth - scrollX, 0, Math.abs(mMenuViewWidth - scrollX));
} else { // 其他情况则关闭
mScroller.startScroll(scrollX, 0, -scrollX, 0, Math.abs(scrollX));
}
invalidate();
}
mMenuViewWidth = INVALID_CHILD_WIDTH;
mIsSlide = false;
mPosition = INVALID_POSITION;
releaseVelocity(); // 这里之所以会调用,是因为如果前面拦截了,就不会执行ACTION_UP,需要在这里释放追踪
break;
}
return true;
} else {
// 此处防止RecyclerView正常滑动时,还有菜单未关闭
closeMenu();
// Velocity,这里的释放是防止RecyclerView正常拦截了,但是在onTouchEvent中却没有被释放;
// 有三种情况:1.onInterceptTouchEvent并未拦截,在onInterceptTouchEvent方法中,DOWN和UP一对获取和释放;
// 2.onInterceptTouchEvent拦截,DOWN获取,但事件不是被侧滑处理,需要在这里进行释放;
// 3.onInterceptTouchEvent拦截,DOWN获取,事件被侧滑处理,则在onTouchEvent的UP中释放。
releaseVelocity();
}
return super.onTouchEvent(e);
}
private void releaseVelocity() {
if (mVelocityTracker != null) {
mVelocityTracker.clear();
mVelocityTracker.recycle();
mVelocityTracker = null;
}
}
private void obtainVelocity(MotionEvent event) {
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
}
mVelocityTracker.addMovement(event);
}
public int pointToPosition(int x, int y) {
int firstPosition = ((LinearLayoutManager) getLayoutManager()).findFirstVisibleItemPosition();
Rect frame = mTouchFrame;
if (frame == null) {
mTouchFrame = new Rect();
frame = mTouchFrame;
}
final int count = getChildCount();
for (int i = count - 1; i >= 0; i--) {
final View child = getChildAt(i);
if (child.getVisibility() == View.VISIBLE) {
child.getHitRect(frame);
if (frame.contains(x, y)) {
return firstPosition + i;
}
}
}
return INVALID_POSITION;
}
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
mFlingView.scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
invalidate();
}
}
/**
* 将显示子菜单的子view关闭
* 这里本身是要自己来实现的,但是由于不定制item,因此不好监听器点击事件,因此需要调用者手动的关闭
*/
public void closeMenu() {
if (mFlingView != null && mFlingView.getScrollX() != 0) {
mFlingView.scrollTo(0, 0);
}
}
}
周四学习了一下组件化开发,基本流程走了一遍,模块间的通信还没细看,总体感觉是grade 的构建这一块我还是比较陌生的,前几天在家没事也复习了一下Android 的IPC,和activity的启动流程,总体上来讲还是有点模糊的
今天看到了关于RecyclerView的新知识点,关于他的优化
优化bindViewHolder 尽量不做耗时的业务
...