仿美團餓了麼選菜介面實現
阿新 • • 發佈:2019-02-04
本文是在未來大神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:
因為外層佈局同樣是Linearlayout,所以這裡可以直接寫上merge佈局,然後再我們的viewgroup裡進行初始化,然後因為我們的weight屬性在專案裡並不固定,所以我們儘量通過自定義屬性來支援動態的weight變化,所以我們在attrs.xml新建一個自定義屬性:<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>
前兩個屬性顯而易見是我們左右recyclerview的weight屬性,最後一個則是我們懸浮標籤的高度。<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>
然後我們的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!!!!