内容简介:RecyclerView零点突破(自定义LayoutManager篇)--待完成
RecyclerView零点突破(自定义LayoutManager篇)--待完成
RecyclerView零点突破(源码分析篇)--待完成
一、入门级-Adapter:仿QQ消息列表
1.创建一个item布局:
<?xml version="1.0" encoding="utf-8"?> <android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="wrap_content"> <ImageView android:id="@+id/id_iv_qq_head" android:layout_width="@dimen/item_qq_msg_iv_size" android:layout_height="@dimen/item_qq_msg_iv_size" android:layout_marginStart="@dimen/dp_16" android:layout_marginTop="@dimen/dp_8" android:layout_marginBottom="@dimen/dp_8" android:src="@mipmap/head" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent"/> <android.support.constraint.ConstraintLayout android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginStart="@dimen/dp_16" android:layout_marginEnd="@dimen/dp_16" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toEndOf="@id/id_iv_qq_head" app:layout_constraintTop_toTopOf="parent"> <TextView android:id="@+id/id_tv_qq_name" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="张风捷特烈" android:textSize="@dimen/sp_16"/> <TextView android:id="@+id/id_tv_time" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="2018-11-30" android:textSize="@dimen/sp_12" app:layout_constraintBottom_toBottomOf="@id/id_tv_qq_name" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="@id/id_tv_qq_name"/> <TextView android:id="@+id/id_tv_info" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="@dimen/dp_4" android:text="天下无双" android:textColor="#aaa" android:textSize="@dimen/sp_12" app:layout_constraintTop_toBottomOf="@id/id_tv_qq_name"/> <com.toly1994.test.view.CountTextView android:id="@+id/id_ctv_num" android:layout_width="wrap_content" android:layout_height="wrap_content" app:layout_constraintBottom_toBottomOf="@id/id_tv_info" app:layout_constraintEnd_toEndOf="@+id/id_tv_time" app:layout_constraintTop_toTopOf="@+id/id_tv_info" app:z_bg_color="@color/red" app:z_ctv_font_size="@dimen/sp_14" app:z_ctv_num="10"/> </android.support.constraint.ConstraintLayout> </android.support.constraint.ConstraintLayout> 复制代码
2.创建ViewHolder
至于为什么要ViewHolder,这里简单的讲一下:
item的布局里面有控件,将这些控件封装起来放在一个类中,使用的时候相当于对成员变量的使用
避免用一次找一次。就像你通过身份证(id)去找人,不用ViewHolder的话,你每次用他都要去找一次
使用ViewHolder相当于把他放旁边放着,使用的时候直接拿,就不需要用id再去找他了
/** * ViewHolder */ class MyViewHolder extends RecyclerView.ViewHolder { private TextView mItemTV; public MyViewHolder(View itemView) { super(itemView); mItemTV = itemView.findViewById(R.id.id_tv_qq_name); } } 复制代码
3.适配器:Adapter
/** * 作者:张风捷特烈<br/> * 时间:2018/12/2 0002:9:22<br/> * 邮箱:1981462002@qq.com<br/> * 说明:qq信息列表Adapter */ public class QQRvAdapter extends RecyclerView.Adapter<QQRvAdapter.MyViewHolder> { private static final String TAG = "TolyRvAdapter"; private List<String> mData; private Context mContext; public QQRvAdapter(List<String> data) { mData = data; } @NonNull @Override//将item布局文件与ViewHolder结合 public MyViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { mContext = parent.getContext(); View view = LayoutInflater.from(mContext) .inflate(R.layout.item_qq_msg, parent, false); return new MyViewHolder(view); } @Override public void onBindViewHolder(@NonNull MyViewHolder holder, int position) { holder.mItemTV.setText(mData.get(position)); } @Override public int getItemCount() { return mData.size(); } } 复制代码
4.使用
mIdRvContent.addItemDecoration(new RecycleViewDivider(this, LinearLayoutManager.VERTICAL));//分割线 mACAdapter = new QQRvAdapter(DataUtils.getRandomName(60, true));//初始化适配器 mIdRvContent.setAdapter(mACAdapter);//设置适配器 mIdRvContent.setLayoutManager(new LinearLayoutManager(this)); 复制代码
二、入门级-LayoutManager(仿淘宝商品列表)
瀑布流 | 网格流 |
---|---|
1.创建一个item布局:
<?xml version="1.0" encoding="utf-8"?> <android.support.v7.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_margin="@dimen/dp_8" app:cardCornerRadius="@dimen/dp_8" app:cardElevation="@dimen/dp_4" app:cardPreventCornerOverlap="false"> <android.support.constraint.ConstraintLayout android:layout_width="match_parent" android:layout_height="wrap_content"> <ImageView android:id="@+id/id_iv_goods" android:layout_width="match_parent" android:layout_height="wrap_content" android:adjustViewBounds="true" android:scaleType="fitStart" android:src="@mipmap/pic1" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent"/> <TextView android:id="@+id/id_iv_info" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_margin="@dimen/dp_8" android:text="蓝夜皮肤,2030年爆款,限额三件,先到先得,售完为止" android:textColor="@color/black" android:textSize="@dimen/sp_12" app:layout_constraintEnd_toEndOf="@id/id_iv_goods" app:layout_constraintStart_toStartOf="@id/id_iv_goods" app:layout_constraintTop_toBottomOf="@id/id_iv_goods"/> <android.support.constraint.ConstraintLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_margin="@dimen/dp_8" android:orientation="horizontal" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/id_iv_info"> <TextView android:id="@+id/id_yang" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginBottom="@dimen/dp_2" android:text="¥" android:textColor="@color/red" android:textSize="@dimen/sp_12" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent"/> <TextView android:id="@+id/id_goods_price" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="99999" android:textColor="@color/red" android:textSize="@dimen/sp_18" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toEndOf="@id/id_yang"/> <TextView android:id="@+id/id_goods_buy_num" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="@dimen/dp_8" android:text="1人已付款" android:textColor="@color/gray_8f" android:textSize="@dimen/sp_12" app:layout_constraintBottom_toBottomOf="@id/id_yang" app:layout_constraintStart_toEndOf="@id/id_goods_price"/> <ImageView android:id="@+id/id_iv_btn_more" android:layout_width="10dp" android:layout_height="10dp" android:src="@drawable/icon_more_h" android:tint="@color/gray_8f" app:layout_constraintBottom_toBottomOf="@id/id_goods_buy_num" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="@id/id_goods_buy_num"/> </android.support.constraint.ConstraintLayout> </android.support.constraint.ConstraintLayout> </android.support.v7.widget.CardView> 复制代码
2.数据准备
/** * 作者:张风捷特烈<br/> * 时间:2018/12/4 0004:8:43<br/> * 邮箱:1981462002@qq.com<br/> * 说明:商品的实体类 */ public class GoodsBean { private int imgId; //资源id private String info; //详情 private float price;//价格 private int buyNum;//购买人数 private String ticket;//优惠券 //其他,略.... } 复制代码
public class BeanFactory { public static List<GoodsBean> getGoodsBean() { List<GoodsBean> beans = new ArrayList<>(); for (int i = 0; i < 4; i++) { beans.add(new GoodsBean(R.mipmap.pic4, "混沌战士,等比例人形,附加刀及盔甲,2030年爆款,限额三百件,先到先得,售完为止", 6666, 277, "店铺优惠,满100送10")); beans.add(new GoodsBean(R.mipmap.pic1, "蓝夜皮肤,2030年爆款,限额三件,先到先得,售完为止", 99999, 2)); beans.add(new GoodsBean(R.mipmap.pic3, "古典美女,等比例人形,2030年爆款,限额三百件,先到先得,售完为止", 999, 177, "店铺优惠,满100送1000")); beans.add(new GoodsBean(R.mipmap.pic6, "珍藏,非卖品", 9999999, 1)); beans.add(new GoodsBean(R.mipmap.pic2, "黑夜皮肤,附加魔法加成,2030年爆款,限额三百件,先到先得,售完为止", 8888, 277, "店铺优惠,满100送100000")); beans.add(new GoodsBean(R.mipmap.pic5, "买洞爷湖送银时,只要998,绝对良心价,2030年爆款,限额三百件,先到先得,售完为止", 998, 277, "店铺优惠,满100送100000")); } return beans; } } 复制代码
public class GoodsAdapter extends RecyclerView.Adapter<GoodsAdapter.MyViewHolder> { private Context mContext; private List<GoodsBean> mData; public GoodsAdapter(List<GoodsBean> data) { mData = data; } @NonNull @Override public MyViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { mContext = parent.getContext(); View view = LayoutInflater.from(mContext).inflate(R.layout.item_goods_list, parent, false); return new MyViewHolder(view); } @Override public void onBindViewHolder(@NonNull MyViewHolder holder, int position) { GoodsBean str = mData.get(position); holder.mIdIvGoods.setImageResource(str.getImgId()); holder.mIdGoodsPrice.setText(str.getPrice()+""); holder.mIdIvInfo.setText(str.getInfo()); holder.mIdGoodsBuyNum.setText(str.getBuyNum()+"人已付款"); } @Override public int getItemCount() { return mData.size(); } class MyViewHolder extends RecyclerView.ViewHolder { public ImageView mIdIvGoods; public TextView mIdYang; public TextView mIdGoodsPrice; public TextView mIdIvInfo; public TextView mIdGoodsBuyNum; public ImageView mIdIvBtnMore; public MyViewHolder(View itemView) { super(itemView); mIdIvGoods = itemView.findViewById(R.id.id_iv_goods); mIdYang = itemView.findViewById(R.id.id_yang); mIdGoodsPrice = itemView.findViewById(R.id.id_goods_price); mIdIvInfo = itemView.findViewById(R.id.id_iv_info); mIdGoodsBuyNum = itemView.findViewById(R.id.id_goods_buy_num); mIdIvBtnMore = itemView.findViewById(R.id.id_iv_btn_more); } } } 复制代码
4.使用:
//初始化RecyclerView @BindView(R.id.id_rv_goods) RecyclerView mIdRvGoods; //使用 mIdRvGoods.setAdapter(new GoodsAdapter(BeanFactory.getGoodsBean())); mIdRvGoods.setLayoutManager(new StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL)); 复制代码
//网格流 mIdRvGoods.setLayoutManager( new GridLayoutManager(this, 3, GridLayoutManager.VERTICAL, false)); 复制代码
二、初级(Item操作效果):
1.点击+水波纹
//为holder.itemView(即条目的View)设置点击事件 @Override public void onBindViewHolder(@NonNull MyViewHolder holder, int position) { String name = mData.get(position); holder.mItemTV.setText(name); holder.itemView.setOnClickListener(v -> { ToastUtil.show(mContext, "第" + position + "个:" + name); }); } 复制代码
//item的最顶层设置: android:background="?android:attr/selectableItemBackground" 复制代码
番外
:你也可以自定义水波纹效果(安卓5.0+)
<?xml version="1.0" encoding="utf-8"?> <ripple xmlns:android="http://schemas.android.com/apk/res/android" android:color="#8834C4F2"><!-- press和水波纹的颜色 --> <item> <shape android:innerRadius="5dp" android:shape="rectangle"> <solid android:color="@color/white"/> <corners android:radius="1dp"/> </shape> </item> </ripple> 复制代码
2.条目的删除和移动操作:
好吧,炫到晃眼
删除条目 | 移动条目 |
---|---|
1).先定义操作接口: AdapterItemOp
/** * 作者:张风捷特烈<br/> * 时间:2018/12/3 0003:20:20<br/> * 邮箱:1981462002@qq.com<br/> * 说明:Item 操作的接口 */ public interface AdapterItemOp<T> { /** * 交换条目 * * @param from 起点 * @param to 终点 */ void onItemMove(int from, int to); /** * 删除条目 * * @param position 位置 */ void onItemDelete(int position); } 复制代码
2).自定义条目触摸时的回调
/** * 作者:张风捷特烈<br/> * 时间:2018/12/3 0003:20:20<br/> * 邮箱:1981462002@qq.com<br/> * 说明:条目触摸时的回调 */ public class ItemTouchCallback extends ItemTouchHelper.Callback { private AdapterItemOp mAdapter;//操作接口 public ItemTouchCallback(AdapterItemOp adapter) { mAdapter = adapter; } @Override public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) { //上下左右拖动 int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN | ItemTouchHelper.RIGHT | ItemTouchHelper.LEFT; //可向左滑动---删除 int swipeFlags = ItemTouchHelper.LEFT; return makeMovementFlags(dragFlags, swipeFlags); } @Override//长按拖动 public boolean isLongPressDragEnabled() { return true; } @Override//滑动删除 public boolean isItemViewSwipeEnabled() { return true; } @Override public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) { //移动时:---交换两个ViewHolder的位置 mAdapter.onItemMove(viewHolder.getAdapterPosition(), target.getAdapterPosition()); return true; } @Override public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) { //滑动删除时:--- mAdapter.onItemDelete(viewHolder.getAdapterPosition()); } } 复制代码
3).GoodsAdapter实现AdapterItemOp接口
用接口为了方便使用,在GoodsAdapter里直接写操作方法,ItemTouchCallback传入GoodsAdapter也可以
不过当其他的Adapter也需要操作时,要修改ItemTouchCallback,所以两者耦合度太高
@Override public void onItemMove(int from, int to) { //交换位置 ToastUtil.showAtOnce(mContext, "已交换:" + from + "和" + to + "的位置"); Collections.swap(mData, from, to); notifyItemMoved(from, to);//刷新移动数据---将不刷新position } @Override public void onItemDelete(int position) { //移除数据 ToastUtil.showAtOnce(mContext, "已删除:" + position); mData.remove(position); notifyItemRemoved(position);//刷新移除数据---将不刷新position } 复制代码
5.条目的添加:(adapter)
注:可以将addItem方法也放在操作接口
注意: notifyItemRemoved
、 notifyItemInserted
和n otifyItemMoved
调用时,item所在的position是不更新的
public void addItem(int position, GoodsBean bean) { mData.add(position, bean); notifyItemInserted(position);//刷新插入数据---将不刷新position if (position == 0) { mRecyclerView.scrollToPosition(0); } } 复制代码
三、初级-多条目(仿微信聊天)
多个条目 | 类型分析 |
---|---|
1.三个布局
1).布局:type0
<?xml version="1.0" encoding="utf-8"?> <android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginBottom="@dimen/dp_16"> <ImageView android:id="@+id/id_iv_chat_head" android:layout_width="@dimen/item_qq_msg_iv_size" android:layout_height="@dimen/item_qq_msg_iv_size" android:layout_marginEnd="@dimen/dp_8" android:layout_marginBottom="@dimen/dp_8" android:src="@mipmap/head" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="parent"/> <TextView android:id="@+id/id_tv_chat_msg" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginEnd="@dimen/dp_4" android:background="@mipmap/me" android:gravity="center" android:maxWidth="280dp" android:text="天下无双" android:textColor="@color/black" android:textSize="@dimen/sp_16" app:layout_constraintEnd_toStartOf="@+id/id_iv_chat_head" app:layout_constraintTop_toTopOf="@+id/id_iv_chat_head"/> </android.support.constraint.ConstraintLayout> 复制代码
2).布局:type1
<?xml version="1.0" encoding="utf-8"?> <android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginBottom="@dimen/dp_16"> <ImageView android:id="@+id/id_iv_chat_head" android:layout_width="@dimen/item_qq_msg_iv_size" android:layout_height="@dimen/item_qq_msg_iv_size" android:layout_marginStart="@dimen/dp_8" android:layout_marginBottom="@dimen/dp_8" android:src="@mipmap/icon_gql" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent"/> <TextView android:id="@+id/id_tv_chat_msg" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="@dimen/dp_4" android:background="@mipmap/he" android:gravity="center" android:maxWidth="280dp" android:text="天下无双" android:textColor="@color/black" android:textSize="@dimen/sp_16" app:layout_constraintStart_toEndOf="@+id/id_iv_chat_head" app:layout_constraintTop_toTopOf="@+id/id_iv_chat_head"/> </android.support.constraint.ConstraintLayout> 复制代码
3).布局:type2
<?xml version="1.0" encoding="utf-8"?> <android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_marginBottom="@dimen/dp_8" android:layout_height="wrap_content"> <TextView android:id="@+id/id_tv_time" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="8dp" android:layout_marginEnd="8dp" android:background="@drawable/shape_round_rect" android:padding="@dimen/dp_4" android:text="12月1日 早上11:45" android:textColor="@color/white" android:textSize="@dimen/sp_14" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent"/> </android.support.constraint.ConstraintLayout> 复制代码
番外
:圆角矩形的shape
<?xml version="1.0" encoding="utf-8"?> <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle"> <solid android:color="#88888888"/> <corners android:radius="4dp"/> </shape> 复制代码
2.数据的初始化
1).设计Adapter中数据的实体类
/** * 作者:张风捷特烈<br/> * 时间:2018/12/3 0003:16:22<br/> * 邮箱:1981462002@qq.com<br/> * 说明:消息的实体类 */ public class MsgBean { private int type;//类型:0 我 1 他 2 时间 private String msg;//信息体 //get、set、构造,略... } 复制代码
2).准备数据
注:聊天数据从: 《匆匆》
中获取0~100个字随机拼接
/** * 获取随机聊天消息 * @return */ public static List<MsgBean> getMsgBeans() { List<MsgBean> beans = new ArrayList<>(); for (int i = 0; i < 60; i++) { if (i % 10 == 0) { beans.add(new MsgBean(2, "")); continue; } beans.add(new MsgBean(ZRandom.rangeInt(0, 1), ZRandom.randomChar(ZData.congcong, 100))); } return beans; } 复制代码
3.适配器:Adapter
/** * 作者:张风捷特烈<br/> * 时间:2018/12/2 0002:9:22<br/> * 邮箱:1981462002@qq.com<br/> * 说明:聊天的适配器 */ public class ChatRvAdapter extends RecyclerView.Adapter<ChatRvAdapter.MyViewHolder> { private static final String TAG = "TolyRvAdapter"; private List<MsgBean> mData; private Context mContext; public ChatRvAdapter(List<MsgBean> data) { mData = data; } @NonNull @Override//将item布局文件与ViewHolder结合 public MyViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { mContext = parent.getContext(); View view = null; switch (viewType) {//对不同的viewType加载不同的布局 case 0: view = LayoutInflater.from(mContext) .inflate(R.layout.item_chat_me, parent, false); break; case 1: view = LayoutInflater.from(mContext) .inflate(R.layout.item_chat_he, parent, false); break; case 2: view = LayoutInflater.from(mContext) .inflate(R.layout.item_chat_time, parent, false); break; } return new MyViewHolder(view); } @Override public void onBindViewHolder(@NonNull MyViewHolder holder, int position) { MsgBean msgBean = mData.get(position); switch (msgBean.getType()) {//对不同的viewType设置不同的数据 case 0: case 1: holder.mItemTV.setText(msgBean.getMsg()); break; case 2: String time = new SimpleDateFormat("MM月dd日 a HH:mm", Locale.CHINA) .format(System.currentTimeMillis()); holder.mItemTvTime.setText(time); break; } } @Override public int getItemCount() { return mData.size(); } @Override public int getItemViewType(int position) {//返回条目种类viewType return mData.get(position).getType(); } /** * ViewHolder */ class MyViewHolder extends RecyclerView.ViewHolder { private TextView mItemTV; private TextView mItemTvTime; public MyViewHolder(View itemView) { super(itemView); mItemTV = itemView.findViewById(R.id.id_tv_chat_msg); mItemTvTime = itemView.findViewById(R.id.id_tv_time); } } } 复制代码
4.使用:
mIdRvContent.setAdapter(new ChatRvAdapter(getMsgBeans())); mIdRvContent.setLayoutManager(new LinearLayoutManager(this)); 复制代码
四、滑动监听:
newState
: 1:开始下滑
--- 0:手指离开屏幕
--- 2:手指离开屏幕,但还在滑动
dx
, dy
每次移动的距离
mIdRvContent.addOnScrollListener(new RecyclerView.OnScrollListener() { @Override public void onScrollStateChanged(RecyclerView recyclerView, int newState) { super.onScrollStateChanged(recyclerView, newState); ToastUtil.show(V012_QQMsgActivity.this, "newState:" + newState); } @Override public void onScrolled(RecyclerView recyclerView, int dx, int dy) { super.onScrolled(recyclerView, dx, dy); } }); 复制代码
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- 【突破面试】你使用过哪些数据分析的方法?
- Go 指针的使用限制和 unsafe.Pointer 的突破之路
- 突破自己
- 这一年,国产技术不断突破
- 如何突破商品期货Tick接收限制
- 让系统内核突破512字节的限制
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
YES!产品经理(上、下册)
汤圆、老马 / 电子工业出版社 / 2011-9-1 / 128.00元
《YES!产品经理(套装上下册)》是一本融合了经管、工具和职场小说特点的图书,作者是国内产品经理咨询界最有实力的团队。 《YES!产品经理(套装上下册)》以职场小说的形式全面介绍产品管理、产品经理相关的知识,所有的问答均放置在设计好的101个情节中,同时每一个情节之间也都有相应的联系,读者能够从具体的情节走向中不但了解到产品管理的完整知识,而且能够深刻感受到一个产品经理的现实工作状态,从知识......一起来看看 《YES!产品经理(上、下册)》 这本书的介绍吧!