原有设计
这段时间接手了公司用户端首页的版本迭代任务.产品设计与上一个版本有非常大的差异.
首先想到的肯定是想通过在原有基础上进行修改. 原有设计是 顶部搜索栏+SlidingTabLayout+ViewPager
实现顶部tab,内容页为RecycleView 头部为banner图加店铺列表+某种商品列表 ,
列表内容为有分类头部的item.CoordinatorLayout+AppBarLayout实现顶部悬浮.
现有的设计
在搜索栏上增加一条公司标语,搜索和viewPager+tab不变 ,当列表滚动到第一个分类item时显示分类tab并实
现根据滚动对分类tab进行切换.实现的功能就是一个滚动与点击切换分类的联动效果.遇到的问题时如何计算第
一个可见item距离顶部的距离, 内容页 我设计成一个多布局item的recycleView .非联动相关的头部 banner+活动选项
+其它组成头部item , 然后是店铺列表item ,某种特殊商品列表item ,一个宫格样式商品列表item,最后是相同样式的的
商品item 顶部分类图标+内容列表 样式相同 只是进行了分类 ,这里共用一个item.
遇到的问题
由于之前没有接手过,看别人代码还是痛苦的,开始在原有的代码上进行修改,首先当然是修改布局,进去xml中简直看懵了,我问了之前做的同事,他说里面有两套布局,一套显示定位失败的情况,一套显示正常然后显示首页内容的布局.这样的写法对于接手的人当然是痛苦的, 反复尝试了几次最后发现还不如自己重写,因为项目进度很赶,布局就让我浪费了两天时间,决定自己重写. 对两套布局以及头部 进行了抽取,通过includ标签进行引入.立马整个首页的布局结构就清晰了,之后遇到多次布局调整都能够快速的进行调整,提高了可维护性,所以希望看到我文章的人也能够有这种意识.
布局设计完成接下来就是要根据UI设计交互的要求实现相应的设计效果
从上到下一个个功能实现进行介绍实现
1 SlidingTabLayout tab分类实现顶部悬浮, 公司标语,搜索栏隐藏
实现方式为系统提供的配置方式CoordinatorLayout+AppBarLayout +内容
CoordinatorLayout需要包裹 AppBarLayout +内容,CoordinatorLayout必须要能占满整个屏幕高度,假如CoordinatorLayout上方还有控件将其顶下去了,那就加上margain_top =-xxdp,AppBarLayout中为顶部悬浮会隐藏的内容.
2 分类联动列表 相当于商城app的左右分类联动切换,只是换成了顶部.开始也只是简简单单的将联动代码迁移过来,之前的商品分类的联动功能是我实现的,完成后发现了很多问题,
1,点击顶部联动tab出现滚动过头,也是出现分类图标被遮挡.
2,列表滚动到顶部实现对顶部tab的切换,但分类图标被遮挡了一部分.
解决第一个问题比较简单,因为有大神提供了解决方法 ,替换自己的LinearLayoutManager即可
滚动到指定位置mRecyclerView.smoothScrollToPosition(position );并置顶.
public class TopLayoutManager extends LinearLayoutManager {
public TopLayoutManager(Context context) {
super(context);
}
public TopLayoutManager(Context context, int orientation, boolean reverseLayout) {
super(context, orientation, reverseLayout);
}
public TopLayoutManager(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
@Override
public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state, int position) {
RecyclerView.SmoothScroller smoothScroller = new TopSmoothScroller(recyclerView.getContext());
smoothScroller.setTargetPosition(position);
startSmoothScroll(smoothScroller);
}
private static class TopSmoothScroller extends LinearSmoothScroller {
TopSmoothScroller(Context context) {
super(context);
}
/**
* 以下参数以LinearSmoothScroller解释
*
* @param viewStart RecyclerView的top位置
* @param viewEnd RecyclerView的bottom位置
* @param boxStart Item的top位置
* @param boxEnd Item的bottom位置
* @param snapPreference 判断滑动方向的标识
* area. One of {@link #SNAP_TO_START}, {@link #SNAP_TO_END} or
* {@link #SNAP_TO_END}.)
* @return 移动偏移量
*/
@Override
public int calculateDtToFit(int viewStart, int viewEnd, int boxStart, int boxEnd, int snapPreference) {
CLog.e("tag",boxStart+"---boxStart-------viewStart-------"+viewStart);
return boxStart - viewStart + 120;// 这里是关键,得到的就是置顶的偏移量
}
}
}
第二个问题处理起来就复杂了
要实现的就是恰好滚到到联动tab的高度就进行切换,既然是滚动,那就得对recycleView的滚动进行监听
mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
}
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
if (dy > 0) {
refreshQuickTop(true);//dy 正值手指滑动方向向上 反之向下
isUp = true;
} else {
refreshQuickTop(false);
isUp = false;
}
RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
// 只有LinearLayoutManager才有查找第一个和最后一个可见view位置的方法
if (layoutManager instanceof LinearLayoutManager) {
LinearLayoutManager linearManager = (LinearLayoutManager) layoutManager;
//获取第一个可见view的位置
int firstItemPosition = linearManager.findFirstVisibleItemPosition();
if (otherAdapter.getData().size() == 0) {
return;
}
//根据索引来获取对应的itemView
View firstVisiableChildView = linearManager.findViewByPosition(firstItemPosition);
//获取当前显示条目的高度
int itemHeight = firstVisiableChildView.getHeight();
//获取当前Recyclerview 偏移量
int flag = firstVisiableChildView.getTop();// 向上滚动 flag 绝对值减小 反之绝对值增大 (关键) // DisplayUtils.dip2px(getActivity(), 40))联动tab高度
if (firstItemPosition == 0) {
if (isVisible && Math.abs(flag) > (itemHeight - DisplayUtils.dip2px(getActivity(), 40))) {
rvTypeTab.setVisibility(View.VISIBLE);
isVisible = false;
} else if (!isVisible && Math.abs(flag) < itemHeight - DisplayUtils.dip2px(getActivity(), 40)){
isVisible = true;
rvTypeTab.setVisibility(View.INVISIBLE);
otherAdapter.setListTabComeBack(0);
}
}
if (isOnItemClick) {//点击联动tab时不进行滚动时的操作
//内容滚动到条目
isOnItemClick = false;
rvTypeTab.scrollToPosition(position);
if (isScroll) {
//改变联动tab选择的颜色和背景
tabAdapter.setSelected(position);
}
isScroll = true;
} else {
if (isUp && Math.abs(flag) > itemHeight - DisplayUtils.dip2px(getActivity(), 44)) {
//获取到第一个条目的类型
if (firstItemPosition != -1) {
String catId = otherAdapter.getData().get(firstItemPosition + 1).getCatName();
//遍历左边列表数据集合,获取到当前类型的索引
for (int i = 0; i < tabAdapter.getData().size(); i++) {
if (catId.equals(tabAdapter.getData().get(i))) {
position = i;
break;
}
}
}
} else if (!isUp && itemHeight - Math.abs(flag) > DisplayUtils.dip2px(getActivity(), 44)) {
// 底部可见
if (firstItemPosition >= 0) {
String catId = otherAdapter.getData().get(firstItemPosition).getCatName();
//遍历左边列表数据集合,获取到当前类型的索引
for (int i = 0; i < tabAdapter.getData().size(); i++) {
if (catId.equals(tabAdapter.getData().get(i))) {
position = i;
break;
}
}
}
}
isOnItemClick = false;
//联动tab滚动到指定条目
rvTypeTab.scrollToPosition(position);
if (isScroll) {
//改变选择的颜色和背景
tabAdapter.setSelected(position);
}
isScroll = true;
}
}
}
});
3 图文混排遇到的问题
图文混排导致文字被图片替换,以及图片和文字不能居中对齐
处理结果:
int count = 0;
SpannableStringBuilder span = new SpannableStringBuilder(item.productName + "/" + item.weightDesc);
CustomImageSpan image = new CustomImageSpan(mContext, R.drawable.lab_recommend_ic, CustomImageSpan.ALIGN_BASELINE);
span.insert(count, "图 ");
count = count + 1;
span.setSpan(image, count-1, count, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
图文居中对齐
public class CustomImageSpan extends ImageSpan {
//自定义对齐方式--与文字中间线对齐
private int ALIGN_FONTCENTER = 2;
public CustomImageSpan(Context context, int resourceId) {
super(context, resourceId);
}
public CustomImageSpan(Context context, int resourceId, int verticalAlignment) {
super(context, resourceId, verticalAlignment);
}
@Override
public void draw(Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom,
Paint paint) {
//draw 方法是重写的ImageSpan父类 DynamicDrawableSpan中的方法,在DynamicDrawableSpan类中,虽有getCachedDrawable(),
// 但是私有的,不能被调用,所以调用ImageSpan中的getrawable()方法,该方法中 会根据传入的drawable ID ,获取该id对应的
// drawable的流对象,并最终获取drawable对象
Drawable drawable = getDrawable(); //调用imageSpan中的方法获取drawable对象
canvas.save();
//获取画笔的文字绘制时的具体测量数据
Paint.FontMetricsInt fm = paint.getFontMetricsInt();
//系统原有方法,默认是Bottom模式)
int transY = bottom - drawable.getBounds().bottom;
if (mVerticalAlignment == ALIGN_BASELINE) {
transY -= fm.descent;
} else if (mVerticalAlignment == ALIGN_FONTCENTER) { //此处加入判断, 如果是自定义的居中对齐
//与文字的中间线对齐(这种方式不论是否设置行间距都能保障文字的中间线和图片的中间线是对齐的)
// y+ascent得到文字内容的顶部坐标,y+descent得到文字的底部坐标,(顶部坐标+底部坐标)/2=文字内容中间线标
transY = ((y + fm.descent) + (y + fm.ascent)) / 2 - drawable.getBounds().bottom / 2;
}
canvas.translate(x, transY+5);
drawable.draw(canvas);
canvas.restore();
}
/**
* 重写getSize方法,只有重写该方法后,才能保证不论是图片大于文字还是文字大于图片,都能实现中间对齐
*/
@Override
public int getSize(Paint paint, CharSequence text, int start, int end, Paint.FontMetricsInt fm) {
Drawable d = getDrawable();
Rect rect = d.getBounds();
if (fm != null) {
Paint.FontMetricsInt fmPaint = paint.getFontMetricsInt();
int fontHeight = fmPaint.bottom - fmPaint.top;
int drHeight = rect.bottom - rect.top;
int top = drHeight / 2 - fontHeight / 4;
int bottom = drHeight / 2 + fontHeight / 4;
fm.ascent = -bottom;
fm.top = -bottom;
fm.bottom = top;
fm.descent = top;
}
return rect.right;
}
}
购物车商品添加抛物线动画
public class ThrowCartAnimUtil {
private static final int THROW_DURATION = 1000;
private static final int SCALE_DURATION = 200;
private static final int OFFY = 200;// dp
private Activity mActivity;
private Animator mAnimator;
private View mAnimView;
private View mEndView;
private final SimpleDraweeView mGenericDraweeView;
public ThrowCartAnimUtil(Activity activity, @NonNull View endView) {
mActivity = activity;
mGenericDraweeView = new SimpleDraweeView(mActivity);
// mGenericDraweeView.setBackgroundResource(R.drawable.red_oval);
final int size = DisplayUtils.dip2px(mActivity, 30);
mGenericDraweeView.setLayoutParams(new ViewGroup.LayoutParams(size, size));
mGenericDraweeView.measure(0, 0);
setAnimView(mGenericDraweeView);
mEndView = endView;
}
public void setAnimView(View view) {
mAnimView = view;
}
public void start(View startView,String url) {
start(startView,url, null);
}
public void start(View startView,String url, Animator.AnimatorListener listener) {
if (mAnimator != null && mAnimator.isRunning()) {
mAnimator.end();
}
FrescoUtils.setRequest(mGenericDraweeView,url);
final int animViewWidth = mAnimView.getMeasuredWidth();
final int animViewHeight = mAnimView.getMeasuredHeight();
//获取视图在窗口的位置
int[] startViewLoc = new int[2];
int[] endViewLoc = new int[2];
startView.getLocationInWindow(startViewLoc);
mEndView.getLocationInWindow(endViewLoc);
//将动画view添加到decorView
ViewGroup decorView = (ViewGroup) mActivity.getWindow().getDecorView();
if (mAnimView.getParent() != decorView) {
ViewUtils.removeFromParent(mAnimView);
decorView.addView(mAnimView);
}
mAnimView.setVisibility(View.VISIBLE);
mAnimView.setX(startViewLoc[0]);
mAnimView.setY(startViewLoc[1]);
//扔出动画
final Point startPot = new Point(startViewLoc[0] + startView.getWidth() / 2, startViewLoc[1] + startView.getHeight() / 2);
final Point endPot = new Point(endViewLoc[0] + mEndView.getWidth() / 2, endViewLoc[1] mEndView.getHeight() / 2);
int offY = DisplayUtils.dip2px(mActivity, OFFY);
ScaleAnimation scaleAnimation = new ScaleAnimation(0.6f, 0.1f,0.6f, 0.1f);
scaleAnimation.setInterpolator(new AccelerateInterpolator());
scaleAnimation.setRepeatCount(0);
scaleAnimation.setFillAfter(true);
ValueAnimator throwAnimator = ObjectAnimator.ofObject(new BezierEvaluator(offY), startPot, endPot);
throwAnimator.setInterpolator(new AccelerateInterpolator(0.8f));
throwAnimator.setDuration(THROW_DURATION);
throwAnimator.addUpdateListener(animation -> {
Point point = (Point) animation.getAnimatedValue();
mAnimView.setX(point.x - animViewWidth / 2);
mAnimView.setY(point.y - animViewHeight / 2);
});
throwAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
mAnimView.setVisibility(View.GONE);
}
@Override
public void onAnimationCancel(Animator animation) {
super.onAnimationCancel(animation);
mAnimView.setVisibility(View.GONE);
}
});
//购物车放大缩小动画
AnimatorSet scaleAnimator = new AnimatorSet();
mEndView.setPivotX(mEndView.getWidth() / 2);
mEndView.setPivotY(mEndView.getHeight() / 2);
//缩小
PropertyValuesHolder valuesHolder1 = PropertyValuesHolder.ofFloat("scaleX", 1, 0.8f);
PropertyValuesHolder valuesHolder2 = PropertyValuesHolder.ofFloat("scaleY", 1, 0.8f);
ValueAnimator zoomIn = ObjectAnimator.ofPropertyValuesHolder(mEndView, valuesHolder1, valuesHolder2);
zoomIn.setDuration(SCALE_DURATION / 2);
//放大
PropertyValuesHolder valuesHolder11 = PropertyValuesHolder.ofFloat("scaleX", 0.8f, 1);
PropertyValuesHolder valuesHolder22 = PropertyValuesHolder.ofFloat("scaleY", 0.8f, 1);
ValueAnimator zoomOut = ObjectAnimator.ofPropertyValuesHolder(mEndView, valuesHolder11,valuesHolder22);
zoomOut.setDuration(SCALE_DURATION / 2);
zoomOut.setInterpolator(new OvershootInterpolator());
scaleAnimator.playSequentially(zoomIn, zoomOut);
AnimatorSet animatorSet = new AnimatorSet();
mAnimator = animatorSet;
// animatorSet.
animatorSet.playSequentially(throwAnimator, scaleAnimator);
if (listener != null) {
animatorSet.addListener(listener);
}
animatorSet.start();
}
public boolean isRunning() {
return mAnimator != null && mAnimator.isRunning();
}
public static ObservableTransformer requestWhitAnim(Context context, View view,String url, ThrowCartAnimUtil anim) {
return upstream -> {
Dialog dialog = Dialogs.progress(context);
return upstream.doOnSubscribe(disposable -> {
if (anim != null) {
anim.start(view,url, new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
//动画结束请求还未完成,弹出等待框
if (!disposable.isDisposed()) {
if (context instanceof Activity && !((Activity) context).isFinishing()) {
dialog.show();
}
dialog.setOnCancelListener(dialog1 -> {
disposable.dispose();
});
}
}
});
}
}).doOnTerminate(dialog::dismiss);
};
}
private static class BezierEvaluator implements TypeEvaluator {
private final Point mPoint = new Point();
private int offsetY;
public BezierEvaluator(int offsetY) {
this.offsetY = offsetY;
}
@Override
public Point evaluate(float fraction, Point startValue, Point endValue) {
final int centerX = (startValue.x + endValue.x) / 2;
final int curX = (int) (startValue.x * Math.pow(1 - fraction, 2) + 2 * fraction * (1 - fraction) * centerX + fraction * fraction * endValue.x);
final int curY = (int) (startValue.y * Math.pow(1 - fraction, 2) + 2 * fraction * (1 - fraction) * (startValue.y - offsetY) + fraction * fraction * endValue.y);
mPoint.set(curX, curY);
return mPoint;
}
}
}