仿美团饿了么选菜界面实现
来源:互联网 发布:江南大学网络教育入口 编辑:程序博客网 时间:2024/05/19 13:08
本文是在未来大神zxt头像狂魔的基础上稍作修改,大家在看这个博客之前可以出门右拐至这里: 传送门-----> 点击打开链接
好了,我们首先看一下两个app的界面长什么样子:
我们看到两个界面都很相似,如果你已经读完了我推荐的博客内容,接下来会非常的简单,首先我们还是无脑的自定义viewgroup,这个界面我打算用两个recyclerview完成,因为是左右布局,我们直接继承Linearlayout然后横向布局即可,所以我们只用这样写:
public class MeiTuanFoodView extends LinearLayout { public MeiTuanFoodView(Context context) { this(context, null); } public MeiTuanFoodView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public MeiTuanFoodView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); setOrientation(HORIZONTAL); }
接下来我们写一个xml布局,既左边一个recyclerview 右边一个recyclerview:
<merge xmlns:android="http://schemas.android.com/apk/res/android" > <android.support.v7.widget.RecyclerView android:layout_width="0px" android:layout_height="match_parent" android:layout_weight="1" android:id="@+id/left_view" ></android.support.v7.widget.RecyclerView> <android.support.v7.widget.RecyclerView android:layout_width="0px" android:layout_height="match_parent" android:layout_weight="2" android:id="@+id/right_view" ></android.support.v7.widget.RecyclerView></merge>因为外层布局同样是Linearlayout,所以这里可以直接写上merge布局,然后再我们的viewgroup里进行初始化,然后因为我们的weight属性在项目里并不固定,所以我们尽量通过自定义属性来支持动态的weight变化,所以我们在attrs.xml新建一个自定义属性:
<resources> <declare-styleable name="MeiTuanFoodView"> <attr name="leftSum" format="integer"></attr> <attr name="rightSum" format="integer"></attr> <attr name="topItemHeight" format="dimension"></attr> </declare-styleable>前两个属性显而易见是我们左右recyclerview的weight属性,最后一个则是我们悬浮标签的高度。
然后我们的viewgroup的代码就变成了如下:
public MeiTuanFoodView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); this.context = context; setOrientation(HORIZONTAL); inflate(context, R.layout.layout_meituan, this); leftView = (RecyclerView) findViewById(R.id.left_view); rightView = (RecyclerView) findViewById(R.id.right_view); leftView.setLayoutManager(new LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)); rightView.setLayoutManager(new LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)); TypedArray ta = context.getTheme().obtainStyledAttributes(attrs, R.styleable.MeiTuanFoodView, defStyleAttr, 0); leftSum = ta.getInt(R.styleable.MeiTuanFoodView_leftSum, 1); rightSum = ta.getInt(R.styleable.MeiTuanFoodView_rightSum, 2); itemTopHeight = ta.getDimensionPixelSize(R.styleable.MeiTuanFoodView_topItemHeight, (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 30, context.getResources().getDisplayMetrics())); ta.recycle(); leftLp = (LayoutParams) leftView.getLayoutParams(); rightLp = (LayoutParams) rightView.getLayoutParams(); if (leftSum != leftLp.weight) { leftLp.weight = leftSum; leftView.setLayoutParams(leftLp); } if (rightSum != rightLp.weight) { rightLp.weight = rightSum; rightView.setLayoutParams(rightLp); } }好了,接下来我们就该动态的设置数据,首先两个recyclerview肯定需要adapter适配器,为了简便,我直接写了简单版的baseAdapter来进行共用,代码非常简单,大家都能看的懂:
public abstract class BaseViewAdapter<T> extends RecyclerView.Adapter<BaseViewAdapter.BaseHolder> { private int selectPosition=-1; private Context context; private List<T> mData; public BaseViewAdapter(Context context,List<T> mData){ this.context=context; this.mData=mData; } @Override public BaseHolder onCreateViewHolder(ViewGroup parent, int viewType) { return new BaseHolder(LayoutInflater.from(context).inflate(getLayoutId(),parent,false)); } @Override public void onBindViewHolder(BaseHolder holder, int position) { bind(holder,position); } public int getSelectPosition() { return selectPosition; } public void setSelectPosition(int selectPosition) { this.selectPosition = selectPosition; notifyDataSetChanged(); } @Override public int getItemCount() { return mData.size(); } protected abstract void bind(BaseHolder holder, int position); public abstract int getLayoutId() ; public static class BaseHolder extends RecyclerView.ViewHolder { private SparseArray<View> mViews = new SparseArray<>(); private View mConvertView; public BaseHolder(View itemView) { super(itemView); mConvertView = itemView; } public <T extends View> T findViews(int resId) { View view = mViews.get(resId); if(null==view){ view=mConvertView.findViewById(resId); mViews.put(resId,view); } return (T)view; } }
我们通过抽象类中的抽象方法把具体的执行过程交给子类去实现,这样我们就可以少写一些代码,愉快的偷懒了。
然后我们开始思考,如何设置数据,当然我们在写项目的时候不需要考虑那么复杂,但是做人嘛,不装逼不斗图不写框架大家还是好朋友吗?
所以我们首先考虑像饿了么这种左右互动左右两列数据应该有个共同的tag或者id来进行匹配,而右边的也需要tag来进行悬浮,考虑到现实项目一般都会有tag,所以我们这里直接用tag来进行判断,于是乎,我们选择通过一个baseBean来进行适配:
public abstract class BaseMeiTuanBean { public abstract String tagStr(); // public abstract long tagInt();}在实际项目中我们左右两列的实体类去继承这个basebean,然后return我们需要的tag数据,这里我写了两个testBean:
public class RightBean extends BaseMeiTuanBean { private String tag; private String text; public String getTag() { return tag; } public void setTag(String tag) { this.tag = tag; } public String getText() { return text; } public void setText(String text) { this.text = text; } @Override public String tagStr() { return tag; } @Override public long tagInt() { return 0; }}
public class LeftBean extends BaseMeiTuanBean { private String tag; private int id; public String getTag() { return tag; } public void setTag(String tag) { this.tag = tag; } public int getId() { return id; } public void setId(int id) { this.id = id; } @Override public String tagStr() { return tag; } @Override public long tagInt() { return 0; }}接下来我们开始写设置数据,我们在自定义view中最后数据来源和界面逻辑都在Activity中进行,所以我们要把通过接口或者抽象类来进行回调,我这里直接用了抽象类来解决,在编写抽象类之前,我们思考一下,我们的抽象类需要什么,首先肯定需要左右两列的实体类,左右两列的list集合,左右两列recyclerview的item布局,左右两列的item点击事件,另外我们左边item一般都会有点击变色的效果,所以还要把点击前后颜色等的变化,所以我们需要左右前后变化的方法等等,于是我写了以下几个方法:
public abstract class BindData<T,E> { public abstract int getLeftLayoutId(); public abstract List<T> getLeftData(); public abstract void bindLeftView(BaseViewAdapter.BaseHolder holder, int position, T bean); public abstract void bindDefaultStatus(BaseViewAdapter.BaseHolder holder, int position,T bean); public abstract void bindSelectStatus(BaseViewAdapter.BaseHolder holder, int position, T bean); public abstract int getRightLayoutId(); public abstract void bindRightView(BaseViewAdapter.BaseHolder holder, int position, E bean); public abstract List<E> getRightData(); public abstract void rightItemClickListener(BaseViewAdapter.BaseHolder holder, int position,E bean); }方法虽然看起来很多,但是都很容易理解,而且不会有很复杂的逻辑,接下来我们要在自定义viewfroup中去设置数据了:
首先我们现对左右recyclerview进行setAdapter:
public void setData(final BindData data) { this.data = data; rightView.addItemDecoration(new MeiTuanItem(context, data)); leftAdapter = new BaseViewAdapter(context, data.getLeftData()) { @Override protected void bind(final BaseHolder holder, final int position) { data.bindLeftView(holder, position, data.getLeftData().get(position)); if (position == getSelectPosition()) { data.bindSelectStatus(holder, position, data.getLeftData().get(position)); } else { data.bindDefaultStatus(holder, position, data.getLeftData().get(position)); } holder.itemView.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { } }); } @Override public int getLayoutId() { return data.getLeftLayoutId(); } }; leftView.setAdapter(leftAdapter); leftView.addOnScrollListener(new RecyclerView.OnScrollListener() { @Override public void onScrolled(RecyclerView recyclerView, int dx, int dy) { super.onScrolled(recyclerView, dx, dy); int lastPosition = ((LinearLayoutManager) recyclerView.getLayoutManager()).findLastVisibleItemPosition(); if(lastPosition==data.getLeftData().size()-1){ leftView.smoothScrollBy(50,50); } } }); rightAdapter = new BaseViewAdapter(context, data.getRightData()) { @Override protected void bind(final BaseHolder holder, final int position) { data.bindRightView(holder, position, data.getRightData().get(position)); holder.itemView.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { data.rightItemClickListener(holder, position, data.getRightData().get(position)); } }); } @Override public int getLayoutId() { return data.getRightLayoutId(); } }; rightView.setAdapter(rightAdapter); }这样就完成了基本的布局编写,如果看了ztx的博客就应该知道这个MeiTuanItem该如何编写,这里我把我的代码贴出来具体逻辑可以去那里充值信仰:
public class MeiTuanItem extends RecyclerView.ItemDecoration { private int mTitleHeight; private BindData data; private Paint mPaint; private Rect mBounds; private int backgroundColor; private int textColor; private int textSize; public MeiTuanItem(Context context,int mTitleHeight, BindData data, int backgroundColor, int textColor,int textSize){ this.mTitleHeight= dip2px(context,mTitleHeight); this.data=data; mPaint=new Paint(); mBounds=new Rect(); this.backgroundColor=backgroundColor; this.textColor=textColor; this.textSize= sp2px(context,textSize); } public MeiTuanItem(Context context,BindData data){ this(context, dip2px(context,10),data, Color.LTGRAY,Color.DKGRAY, sp2px(context,9)); } @Override public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) { super.onDrawOver(c, parent, state); if(parent.getLayoutManager() instanceof LinearLayoutManager){ int position = ((LinearLayoutManager) parent.getLayoutManager()).findFirstVisibleItemPosition(); View itemView = parent.findViewHolderForAdapterPosition(position).itemView; boolean flag=false; if(position+1<data.getRightData().size()){ if(!isTopNotEqualsNext(position)){ if(itemView.getTop()+itemView.getHeight()<mTitleHeight){ c.save(); c.translate(0,itemView.getTop()+itemView.getHeight()-mTitleHeight); } } } mPaint.setColor(backgroundColor); c.drawRect(parent.getPaddingLeft(),parent.getPaddingTop(),parent.getRight()-parent.getPaddingRight(),parent.getTop()+mTitleHeight,mPaint); mPaint.setTextSize(textSize); mPaint.setColor(textColor); mPaint.getTextBounds(getPositionText(position),0,getPositionText(position).length(),mBounds); c.drawText(getPositionText(position),parent.getPaddingLeft(), parent.getPaddingTop() + mTitleHeight - (mTitleHeight / 2 - mBounds.height() / 2),mPaint); } } @Override public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) { super.onDraw(c, parent, state); int paddingLeft = parent.getPaddingLeft(); int paddingRight = parent.getRight() - parent.getPaddingRight(); int childCount = parent.getChildCount(); for (int i=0;i<childCount;i++){ View child = parent.getChildAt(i); RecyclerView.LayoutParams lp= (RecyclerView.LayoutParams) child.getLayoutParams(); int position = lp.getViewLayoutPosition(); if(position==0){ mPaint.setColor(backgroundColor); c.drawRect(paddingLeft,child.getTop()-lp.topMargin-mTitleHeight,paddingRight,child.getTop()-lp.topMargin,mPaint); mPaint.setColor(textColor); mPaint.setTextSize(textSize); mPaint.getTextBounds(getPositionText(position),0,getPositionText(position).length(),mBounds); c.drawText(getPositionText(position),child.getPaddingLeft(),child.getTop()-lp.topMargin-(mTitleHeight/2-mBounds.height()/2),mPaint); }else { if(!isTopNotEqualsBefore(position)){ mPaint.setColor(backgroundColor); c.drawRect(paddingLeft,child.getTop()-lp.topMargin-mTitleHeight,paddingRight,child.getTop()-lp.topMargin,mPaint); mPaint.setColor(textColor); mPaint.setTextSize(textSize); mPaint.getTextBounds(getPositionText(position),0,getPositionText(position).length(),mBounds); c.drawText(getPositionText(position),child.getPaddingLeft(),child.getTop()-lp.topMargin-(mTitleHeight/2-mBounds.height()/2),mPaint); }else { } } } } @Override public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) { super.getItemOffsets(outRect, view, parent, state); int position = ((RecyclerView.LayoutParams) view.getLayoutParams()).getViewLayoutPosition(); if(position==0){ outRect.set(0,mTitleHeight,0,0); }else { if(!isTopNotEqualsBefore(position)){ outRect.set(0,mTitleHeight,0,0); }else { outRect.set(0,0,0,0); } } } public String getPositionText(int position){ if(data.getRightData().get(position) instanceof BaseMeiTuanBean){ return ((BaseMeiTuanBean) data.getRightData().get(position)).tagStr(); } return ""; } public boolean isTopNotEqualsBefore(int position){ if (data.getRightData().get(position) instanceof BaseMeiTuanBean && data.getRightData().get(position-1) instanceof BaseMeiTuanBean) { return ((BaseMeiTuanBean) data.getRightData().get(position)).tagStr().equals(((BaseMeiTuanBean) data.getRightData().get(position-1)).tagStr()); } return true; } public boolean isTopNotEqualsNext(int position){ if (data.getRightData().get(position) instanceof BaseMeiTuanBean && data.getRightData().get(position+1) instanceof BaseMeiTuanBean) { return ((BaseMeiTuanBean) data.getRightData().get(position)).tagStr().equals(((BaseMeiTuanBean) data.getRightData().get(position+1)).tagStr()); } return true; } public static int dip2px(Context context, float dpValue) { final float scale = context.getResources().getDisplayMetrics().density; return (int) (dpValue * scale + 0.5f); } public static int sp2px(Context context, float spValue) { final float fontScale = context.getResources().getDisplayMetrics().scaledDensity; return (int) (spValue * fontScale + 0.5f); }
剩下的就是左右联动的故事了,其实打完这个副本的终极boss也非常简单,我们知道当点击左边的时候我们要让被点击的item变色以及右边recyclerview滑动到指定位置,两者是通过tag进行关联,也就是说当点击的tag等于右边recyclerview第一个等于tag的item时的位置,既然逻辑清晰了,于是我们写个方法来获取recyclerview应该滑动的距离:
public int leftBoundRightPosition(int position) { rightData=data.getRightData(); for (int i=0;i<rightData.size();i++){ if(data.getRightData().get(i) instanceof BaseMeiTuanBean && data.getLeftData().get(position) instanceof BaseMeiTuanBean){ if(rightData.get(i).tagStr().equals(((BaseMeiTuanBean) data.getLeftData().get(position)).tagStr())){ return i; }else { continue; } }else { continue; } } return 0; }这里我们返回了第一个position的位置,所以我们就可以愉快的进行点击了:
holder.itemView.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { setSelectPosition(position); ((LinearLayoutManager) rightView.getLayoutManager()).scrollToPositionWithOffset(leftBoundRightPosition(position),0); //rightView.scrollToPosition(leftBoundRightPosition(position)); //((LinearLayoutManager) rightView.getLayoutManager()).smoothScrollToPosition(rightView,null,position); } });
注释部分的代码大家也可以试试,说不定只是我故意浪费你们时间呢!
然后我们就该获取当右边滑动左边也紧随这区滑动的事件,所以我们这里要对右边的滑动进行监听,当右边的tag等于左边的tag的时候,左边的tag进行滚动,还要注意的是,以为我们是上下滚动,当我们向下滚动的时候应该在tag与下一个tag的临界点进行变化,当我们向上滚动的时候应该是当前tag与上一个tag临界点进行变化,所以我们应该分开判断,首先我们写几个方法:
public boolean isTopNotEqualsBefore(int position) { if (data.getRightData().get(position) instanceof BaseMeiTuanBean && data.getRightData().get(position - 1) instanceof BaseMeiTuanBean) { return ((BaseMeiTuanBean) data.getRightData().get(position)).tagStr().equals(((BaseMeiTuanBean) data.getRightData().get(position - 1)).tagStr()); } return true; } public boolean isTopNotEqualsNext(int position) { if (data.getRightData().get(position) instanceof BaseMeiTuanBean && data.getRightData().get(position + 1) instanceof BaseMeiTuanBean) { return ((BaseMeiTuanBean) data.getRightData().get(position)).tagStr().equals(((BaseMeiTuanBean) data.getRightData().get(position + 1)).tagStr()); } return true; } public int rightBoundLeftPosition(int position) { leftData=data.getLeftData(); for (int i=0;i<leftData.size();i++){ if(data.getRightData().get(position) instanceof BaseMeiTuanBean && data.getLeftData().get(i) instanceof BaseMeiTuanBean){ if(((BaseMeiTuanBean) data.getRightData().get(position)).tagStr().equals(leftData.get(i).tagStr())){ return i; }else { continue; } }else{ continue; } } return 0; }前两个方法是用来判断上下滚动时,tag到达临界点的时候是否相等,第三个则跟上一个一样,获取左边recyclerview需要滚动到的位置。
然后我们去实现滚动监听:
rightView.addOnScrollListener(new RecyclerView.OnScrollListener() { @Override public void onScrolled(RecyclerView recyclerView, int dx, int dy) { super.onScrolled(recyclerView, dx, dy); int position = ((LinearLayoutManager) recyclerView.getLayoutManager()).findFirstVisibleItemPosition(); //Log.e("dy:","dy:"+dy); Log.e("position:", "position:" + position); int lastPosition = ((LinearLayoutManager) recyclerView.getLayoutManager()).findLastVisibleItemPosition(); Log.e("lastPosition:", "lastPosition:" + lastPosition); if (dy > 0) { if (position + 1 < data.getRightData().size()) { if (position == 0) { leftAdapter.setSelectPosition(0); } else { if (isTopNotEqualsNext(position)) { leftAdapter.setSelectPosition(rightBoundLeftPosition(position)); leftView.scrollToPosition(rightBoundLeftPosition(position)); Log.e("rightBoundLeftPosition:","rightBoundLeftPosition:"+rightBoundLeftPosition(position)); // ((LinearLayoutManager) leftView.getLayoutManager()).smoothScrollToPosition(leftView,null,data.rightBoundLeftPosition(data.getRightData().get(position),data.getLeftData())); } } } } else { if (position + 1 < data.getRightData().size()) { if (position == 0) { leftAdapter.setSelectPosition(0); } else { if (isTopNotEqualsBefore(position)) { leftAdapter.setSelectPosition(rightBoundLeftPosition(position)); leftView.scrollToPosition(rightBoundLeftPosition(position)); // ((LinearLayoutManager) leftView.getLayoutManager()).smoothScrollToPosition(leftView,null,data.rightBoundLeftPosition(data.getRightData().get(position),data.getLeftData())); } } } } } });
这样,我们就可以在我们的activity里进行happy的玩耍了,我们在activity的xml里直接声明:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent" xmlns:app="http://schemas.android.com/apk/res-auto" > <i.am.who.xiaomastudyproject.MeiTuanFoodView android:layout_width="match_parent" android:layout_height="match_parent" android:id="@+id/meituan" app:leftSum="2" app:rightSum="5" ></i.am.who.xiaomastudyproject.MeiTuanFoodView></LinearLayout>然后再activity中进行处理:
public class A extends AppCompatActivity { private MeiTuanFoodView meituan; private List<LeftBean> leftData=new ArrayList<>(); private List<RightBean> rightData=new ArrayList<>(); @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.a); meituan= (MeiTuanFoodView) findViewById(R.id.meituan); for (int i=0;i<20;i++){ LeftBean bean=new LeftBean(); bean.setTag("i:"+i); bean.setId(i); leftData.add(bean); } for (int i=0;i<20;i++){ for (int j=0;j<20;j++){ RightBean bean=new RightBean(); bean.setTag("i:"+i); bean.setText("j:"+j+" i:"+i); rightData.add(bean); } } meituan.setData(new BindData<LeftBean,RightBean>() { @Override public int getLeftLayoutId() { return R.layout.item; } @Override public List<LeftBean> getLeftData() { return leftData; } @Override public void bindLeftView(BaseViewAdapter.BaseHolder holder, int position, LeftBean bean) { TextView tv=holder.findViews(R.id.tv); tv.setText(bean.getTag()); } @Override public void bindDefaultStatus(BaseViewAdapter.BaseHolder holder, int position, LeftBean bean) { TextView tv=holder.findViews(R.id.tv); tv.setTextColor(Color.BLACK); tv.setBackgroundColor(Color.WHITE); } @Override public void bindSelectStatus(BaseViewAdapter.BaseHolder holder, int position, LeftBean bean) { TextView tv=holder.findViews(R.id.tv); tv.setTextColor(Color.WHITE); tv.setBackgroundColor(Color.BLACK); } @Override public int getRightLayoutId() { return R.layout.item; } @Override public void bindRightView(BaseViewAdapter.BaseHolder holder, int position, RightBean bean) { TextView tv=holder.findViews(R.id.tv); tv.setText(bean.getText()); } @Override public List<RightBean> getRightData() { return rightData; } @Override public void rightItemClickListener(BaseViewAdapter.BaseHolder holder, int position, RightBean bean) { Toast.makeText(A.this,bean.getText(),Toast.LENGTH_SHORT).show(); } }); }}就是这么简单,我们的自定义view就写好了,只需要公开出来这几个方法就可以基本上实现界面的构造,当然在实际项目中还有许多细节值得优化,但是should i care,反正我只是在愉快的装逼~最后上效果然后跑去愉快的吃午饭了 2016年10月25日 11:58:15!!!!
6 0
- 仿美团饿了么选菜界面实现
- jsp实现了简单的注册界面
- 实现了界面可以像QQ一样像上伸缩
- 仿“饿了么”订餐app界面实现
- 网站界面定下来了
- startx 界面进不去了
- 登录界面做成了
- 大本营换界面了
- 高仿饿了么订餐界面
- 用ListView和ImageList实现了和acdsee98%相似的界面
- android实现应用程序只有在第一次启动时显示引导界面 ,以后就不在显示了
- android实现应用程序只有在第一次启动时显示引导界面 ,以后就不在显示了
- 【Android界面实现】整合了刷新、加载更多、滑动删除功能的XListview
- android实现应用程序只有在第一次启动时显示引导界面 ,以后就不在显示了
- 【Android界面实现】整合了刷新、加载更多、滑动删除功能的XListview
- 简单Java图书管理程序(只实现了界面的显示)
- Android实现应用程序只有在第一次启动时显示引导界面 ,以后就不在显示了
- 实现各种链表操作,写了一个链表操作界面
- Rails Web App Learning in action (2)--the basic version of students selective courses
- 从数据库表反射出实体类,自动生成实体类
- python学习2
- 自然对数e的近似值
- 第八周练习--求值
- 仿美团饿了么选菜界面实现
- [次小生成树]The Unique MST
- ##iOS中的微信支付 支付验证签名失败
- RecycleView嵌套RecycleView点击里面的无法响应CollapsingToolbarLayout
- Java中文件的写入
- 安卓学习笔记---支付宝支付-交易订单处理失败,请稍后再试。(提示ALI59错误)
- Caffe的可视化训练:绘制loss和accuracy曲线
- 【转】Android 使用NDK开发中,遇到memset,memcpy, malloc函数错误
- ORACLE中通过SQL语句(alter table)来增加、删除、修改字段