打造QQ6.X最新版本侧滑界面效果(三十八)

最后更新于:2022-04-01 07:13:45

## (一).前言: 这两天QQ进行了重大更新(6.X)尤其在UI风格上面由之前的蓝色换成了白色居多了,侧滑效果也发生了一些变化,那我们今天来模仿实现一个QQ6.X版本的侧滑界面效果。今天我们还是采用神器ViewDragHelper来实现,之前我们以前基于ViewDragHelper的使用和打造QQ5.X效果了,基本使用方法可以点击下面的连接: * [神器ViewDragHelper完全解析,妈妈再也不担心我自定义ViewGroup滑动View操作啦~(三十三)](http://blog.csdn.net/developer_jiangqq/article/details/50033453) * [神器ViewDragHelper完全解析之详解实现QQ5.X侧滑酷炫效果(三十四)](http://blog.csdn.net/developer_jiangqq/article/details/50043159) 如果对于ViewDragHelper不是特别了解的朋友可以查看上面的文章学习一下。 本次实例具体代码已经上传到下面的项目中,欢迎各位去star和fork一下。 [https://github.com/jiangqqlmj/DragHelper4QQ](https://github.com/jiangqqlmj/DragHelper4QQ) FastDev4Android框架项目地址:[https://github.com/jiangqqlmj/FastDev4Android](https://github.com/jiangqqlmj/FastDev4Android) ## (二).ViewGragHelper的基本使用    前面我们学习ViewGragHelper的基本使用方法,同时也知道了里边的若干个方法的用途,下面我们还是把基本的使用步骤温习一下。要使用ViewGragHelper实现子View拖拽移动的步骤如下: 1. 创建ViewGragHelper实例(传入Callback) 2. 重写事件拦截处理方法onInterceptTouch和onTouchEvent 3. 实现Callback,实现其中的相关方法tryCaptureView以及水平或者垂直方向移动的距离方法 更加具体分析大家可以看前一篇博客,或者我们今天这边会通过具体实例讲解一下。 ## (三).QQ5.X侧滑效果实现分析:   在正式版本QQ中的侧滑效果如下: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569c8ec761f15.jpg) 观察上面我们可以理解为两个View,一个是底部的相当于左侧功能View,另外一个是上层主功能内容View,我们在上面进行拖拽上层View或者左右滑动的时候,上层和下层的View相应进行滑动以及View大小变化,同时加入相关的动画。当然我们点击上层的View可以进行打开或者关闭侧滑菜单。 ## (四).侧滑效果自定义组件实现 1.首先我们这边集成自FrameLayout创建一个自定义View  DragLayout。内部的定义的一些变量如下(主要包括一些配置类,手势,ViewDragHelper实例,屏幕宽高,拖拽的子视图View等) ~~~ //是否带有阴影效果 private boolean isShowShadow = true; //手势处理类 private GestureDetectorCompat gestureDetector; //视图拖拽移动帮助类 private ViewDragHelper dragHelper; //滑动监听器 private DragListener dragListener; //水平拖拽的距离 private int range; //宽度 private int width; //高度 private int height; //main视图距离在ViewGroup距离左边的距离 private int mainLeft; private Context context; private ImageView iv_shadow; //左侧布局 private RelativeLayout vg_left; //右侧(主界面布局) private CustomRelativeLayout vg_main; ~~~    然后在内部还定义了一个回调接口主要处理拖拽过程中的一些页面打开,关闭以及滑动中的事件回调: ~~~ /** * 滑动相关回调接口 */ public interface DragListener { //界面打开 public void onOpen(); //界面关闭 public void onClose(); //界面滑动过程中 public void onDrag(float percent); } ~~~ 2.开始创建ViewDragHelper实例,依然在自定义View DragLayout初始化的时候创建,使用ViewGragHelper的静态方法: ~~~ public DragLayout(Context context,AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); gestureDetector = new GestureDetectorCompat(context, new YScrollDetector()); dragHelper =ViewDragHelper.create(this, dragHelperCallback); } ~~~ 其中create()方法创建的时候传入了一个dragHelperCallBack回调类,将会在第四点中讲到。 3.接着需要重写ViewGroup中事件方法,拦截触摸事件给ViewGragHelper内部进行处理,这样达到拖拽移动子View视图的目的; ~~~ /** * 拦截触摸事件 * @param ev * @return */ @Override public boolean onInterceptTouchEvent(MotionEvent ev) { return dragHelper.shouldInterceptTouchEvent(ev) &&gestureDetector.onTouchEvent(ev); } /** * 将拦截的到事件给ViewDragHelper进行处理 * @param e * @return */ @Override public boolean onTouchEvent(MotionEvent e){ try { dragHelper.processTouchEvent(e); } catch (Exception ex) { ex.printStackTrace(); } return false; } ~~~ 这边我们在onInterceptTouchEvent拦截让事件从父控件往子View中转移,然后在onTouchEvent方法中拦截让ViewDragHelper进行消费处理。 4.开始自定义创建ViewDragHelper.Callback的实例dragHelperCallback分别实现一个抽象方法tryCaptureView以及重写以下若干个方法来实现侧滑功能,下面一个个来看一下。 ~~~ /** * 拦截所有的子View * @param child Child the user is attempting to capture * @param pointerId ID of the pointer attempting the capture * @return */ @Override public boolean tryCaptureView(Viewchild, int pointerId) { return true; } ~~~ 该进行拦截ViewGroup(本例中为:DragLayout)中所有的子View,直接返回true,表示所有的子View都可以进行拖拽移动。 ~~~ /** * 水平方向移动 * @param child Child view beingdragged * @param left Attempted motion alongthe X axis * @param dx Proposed change inposition for left * @return */ @Override public int clampViewPositionHorizontal(View child, int left, int dx) { if (mainLeft + dx < 0) { return 0; } else if (mainLeft + dx >range) { return range; } else { return left; } } ~~~ 实现该方法表示水平方向滑动,同时方法中会进行判断边界值,例如当上面的main view已经向左移动边界之外了,直接返回0,表示向左最左边只能x=0;然后向右移动会判断向右最变得距离range,至于range的初始化后边会讲到。除了这两种情况之外,就是直接返回left即可。 ~~~ /** * 设置水平方向滑动的最远距离 *@param child Child view to check 屏幕宽度 * @return */ @Override public int getViewHorizontalDragRange(View child) { return width; } ~~~ 该方法有必要实现,因为该方法在Callback内部默认返回0,也就是说,如果的view的click事件为true,那么会出现整个子View没法拖拽移动的情况了。那么这边直接返回left view宽度了,表示水平方向滑动的最远距离了。 ~~~ /** * 当拖拽的子View,手势释放的时候回调的方法, 然后根据左滑或者右滑的距离进行判断打开或者关闭 * @param releasedChild * @param xvel * @param yvel */ @Override public void onViewReleased(View releasedChild, float xvel, float yvel) { super.onViewReleased(releasedChild,xvel, yvel); if (xvel > 0) { open(); } else if (xvel < 0) { close(); } else if (releasedChild == vg_main&& mainLeft > range * 0.3) { open(); } else if (releasedChild == vg_left&& mainLeft > range * 0.7) { open(); } else { close(); } } ~~~ 该方法在拖拽子View移动手指释放的时候被调用,这是会判断移动向左,向右的意图,进行打开或者关闭man view(上层视图)。下面是实现的最后一个方法:onViewPositionChanged ~~~ /** * 子View被拖拽 移动的时候回调的方法 * @param changedView View whoseposition changed * @param left New X coordinate of theleft edge of the view * @param top New Y coordinate of thetop edge of the view * @param dx Change in X position fromthe last call * @param dy Change in Y position fromthe last call */ @Override public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) { if (changedView == vg_main) { mainLeft = left; } else { mainLeft = mainLeft + left; } if (mainLeft < 0) { mainLeft = 0; } else if (mainLeft > range) { mainLeft = range; } if (isShowShadow) { iv_shadow.layout(mainLeft, 0,mainLeft + width, height); } if (changedView == vg_left) { vg_left.layout(0, 0, width,height); vg_main.layout(mainLeft, 0,mainLeft + width, height); } dispatchDragEvent(mainLeft); } }; ~~~ 该方法是在我们进行拖拽移动子View的过程中进行回调,根据移动坐标位置,然后进行重新定义left view和main view。同时调用dispathDragEvent()方法进行拖拽事件相关处理分发同时根据状态来回调接口: ~~~ /** * 进行处理拖拽事件 * @param mainLeft */ private void dispatchDragEvent(int mainLeft) { if (dragListener == null) { return; } float percent = mainLeft / (float)range; //根据滑动的距离的比例 animateView(percent); //进行回调滑动的百分比 dragListener.onDrag(percent); Status lastStatus = status; if (lastStatus != getStatus()&& status == Status.Close) { dragListener.onClose(); } else if (lastStatus != getStatus()&& status == Status.Open) { dragListener.onOpen(); } } ~~~ 该方法中有一行代码float percent=mainLeft/(float)range;算到一个百分比后面会用到 5.至于子View布局的获取初始化以及宽高和水平滑动距离的大小设置方法: ~~~ /** * 布局加载完成回调 * 做一些初始化的操作 */ @Override protected void onFinishInflate() { super.onFinishInflate(); if (isShowShadow) { iv_shadow = new ImageView(context); iv_shadow.setImageResource(R.mipmap.shadow); LayoutParams lp = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); addView(iv_shadow, 1, lp); } //左侧界面 vg_left = (RelativeLayout)getChildAt(0); //右侧(主)界面 vg_main = (CustomRelativeLayout)getChildAt(isShowShadow ? 2 : 1); vg_main.setDragLayout(this); vg_left.setClickable(true); vg_main.setClickable(true); } 以及控件大小发生变化回调的方法: @Override protected void onSizeChanged(int w, int h,int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); width = vg_left.getMeasuredWidth(); height = vg_left.getMeasuredHeight(); //可以水平拖拽滑动的距离 一共为屏幕宽度的80% range = (int) (width *0.8f); } ~~~ 在该方法中我们可以实时获取宽和高以及拖拽水平距离。 6.上面的所有核心代码都为使用ViewDragHelper实现子控件View拖拽移动的方法,但是根据我们这边侧滑效果还需要实现动画以及滑动过程中View的缩放效果,所以我们这边引入了一个动画开源库:    ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569c8ec77fa41.jpg) 然后根据前面算出来的百分比来缩放View视图: ~~~ /** * 根据滑动的距离的比例,进行平移动画 * @param percent */ private void animateView(float percent) { float f1 = 1 - percent * 0.5f; ViewHelper.setTranslationX(vg_left,-vg_left.getWidth() / 2.5f + vg_left.getWidth() / 2.5f * percent); if (isShowShadow) { //阴影效果视图大小进行缩放 ViewHelper.setScaleX(iv_shadow, f1* 1.2f * (1 - percent * 0.10f)); ViewHelper.setScaleY(iv_shadow, f1* 1.85f * (1 - percent * 0.10f)); } } ~~~ 7.当然除了上面这些还缺少一个效果就是,当我们滑动过程中假如我们手指释放,按照常理来讲view就不会在进行移动了,那么这边我们需要一个加速度当我们释放之后,还能保持一定的速度,该怎么样实现呢?答案就是实现computeScroll()方法。     ~~~ /** * 有加速度,当我们停止滑动的时候,该不会立即停止动画效果 */ @Override public void computeScroll() { if (dragHelper.continueSettling(true)){ ViewCompat.postInvalidateOnAnimation(this); } } ~~~ OK上面关于DragLayout的核心代码就差不多这么多了,下面是使用DragLayout类来实现侧滑效果啦! #(五).侧滑效果组件使用 1.首先使用的布局文件如下: ~~~ <com.chinaztt.widget.DragLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/dl" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@android:color/transparent" > <!--下层 左边的布局--> <includelayout="@layout/left_view_layout"/> <!--上层 右边的主布局--> <com.chinaztt.widget.CustomRelativeLayout android:layout_width="match_parent" android:layout_height="match_parent" android:background="#FFFFFF" > <LinearLayout android:layout_width="fill_parent" android:layout_height="fill_parent" android:orientation="vertical" > <RelativeLayout android:id="@+id/rl_title" android:layout_width="match_parent" android:layout_height="49dp" android:gravity="bottom" android:background="@android:color/holo_orange_light" > <includelayout="@layout/common_top_bar_layout"/> </RelativeLayout> <!--中间内容后面放入Fragment--> <FrameLayout android:layout_width="fill_parent" android:layout_height="fill_parent" > <fragment android:id="@+id/main_info_fragment" class="com.chinaztt.fragment.OneFragment" android:layout_width="fill_parent" android:layout_height="fill_parent"/> </FrameLayout> </LinearLayout> </com.chinaztt.widget.CustomRelativeLayout> </com.chinaztt.widget.DragLayout> ~~~ 该布局文件中父层View就是DragLayout,然后内部有两个RelativeLayout布局,分别充当下一层布局和上一层主布局。 2.下面我们来看一下下层菜单布局,这边我专门写了一个left_view_layout.xml文件,其中主要分为三块,第一块顶部为头像个人基本信息布局,中间为功能入口列表,底部是设置等功能,具体布局代码如下: ~~~ <RelativeLayout 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="match_parent" android:paddingTop="70dp" android:background="@drawable/sidebar_bg" > <LinearLayout android:id="@+id/ll1" android:paddingLeft="30dp" android:layout_width="fill_parent" android:layout_height="wrap_content" android:orientation="vertical"> <!--头像,昵称信息--> <LinearLayout android:layout_width="match_parent" android:layout_height="70dp" android:orientation="horizontal" android:gravity="center_vertical" > <com.chinaztt.widget.RoundAngleImageView android:id="@+id/iv_bottom" android:layout_width="50dp" android:layout_height="50dp" android:scaleType="fitXY" android:src="@drawable/icon_logo" app:roundWidth="25dp" app:roundHeight="25dp"/> <LinearLayout android:layout_width="fill_parent" android:layout_height="wrap_content" android:gravity="center_vertical" android:layout_gravity="center_vertical" android:orientation="vertical"> <RelativeLayout android:layout_width="fill_parent" android:layout_height="wrap_content" > <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerVertical="true" android:layout_marginLeft="15dp" android:text="名字:jiangqqlmj" android:textColor="@android:color/black" android:textSize="15sp" /> <ImageButton android:layout_alignParentRight="true" android:layout_centerVertical="true" android:layout_marginRight="100dp" android:layout_width="22dp" android:layout_height="22dp" android:background="@drawable/qrcode_selector"/> </RelativeLayout> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_vertical" android:layout_marginLeft="15dp" android:text="QQ:781931404" android:textColor="@android:color/black" android:textSize="13sp" /> </LinearLayout> </LinearLayout> <LinearLayout android:layout_width="fill_parent" android:layout_height="wrap_content" android:gravity="center_vertical" android:orientation="horizontal"> <ImageView android:layout_width="17dp" android:layout_height="17dp" android:scaleType="fitXY" android:src="@drawable/sidebar_signature_nor"/> <TextView android:layout_marginLeft="5dp" android:textSize="13sp" android:textColor="#676767" android:layout_width="fill_parent" android:layout_height="wrap_content" android:text="用心做产品!"/> </LinearLayout> </LinearLayout> <!--底部功能条--> <includelayout="@layout/left_view_bottom_layout" android:id="@+id/bottom_view" /> <!--中间列表--> <ListView android:id="@+id/lv" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_above="@id/bottom_view" android:layout_below="@id/ll1" android:layout_marginBottom="30dp" android:layout_marginTop="70dp" android:cacheColorHint="#00000000" android:listSelector="@drawable/lv_click_selector" android:divider="@null" android:scrollbars="none" android:textColor="#ffffff"/> </RelativeLayout> ~~~ 该布局还是比较简单的,对于上层主内容布局这边就放一个顶部导航栏和中的Fragment内容信息,留着后期大家功能扩展即可。 3.主Activity使用如下: ~~~ public class MainActivity extends BaseActivity { private DragLayout dl; private ListView lv; private ImageView iv_icon, iv_bottom; private QuickAdapter<ItemBean> quickAdapter; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); setStatusBar(); initDragLayout(); initView(); } private void initDragLayout() { dl = (DragLayout) findViewById(R.id.dl); dl.setDragListener(new DragLayout.DragListener() { //界面打开的时候 @Override public void onOpen() { } //界面关闭的时候 @Override public void onClose() { } //界面滑动的时候 @Override public void onDrag(float percent) { ViewHelper.setAlpha(iv_icon, 1 - percent); } }); } private void initView() { iv_icon = (ImageView) findViewById(R.id.iv_icon); iv_bottom = (ImageView) findViewById(R.id.iv_bottom); lv = (ListView) findViewById(R.id.lv); lv.setAdapter(quickAdapter=new QuickAdapter<ItemBean>(this,R.layout.item_left_layout, ItemDataUtils.getItemBeans()) { @Override protected void convert(BaseAdapterHelper helper, ItemBean item) { helper.setImageResource(R.id.item_img,item.getImg()) .setText(R.id.item_tv,item.getTitle()); } }); lv.setOnItemClickListener(new OnItemClickListener() { @Override public void onItemClick(AdapterView<?> arg0, View arg1, int position, long arg3) { Toast.makeText(MainActivity.this,"Click Item "+position,Toast.LENGTH_SHORT).show(); } }); iv_icon.setOnClickListener(new OnClickListener() { @Override public void onClick(View arg0) { dl.open(); } }); } } ~~~ 初始化控件,设置滑动监听器,以及左侧菜单功能列表设置即可了,不过上面大家应该看了QuickAdapter的使用,该为BaseAdapterHelper框架使用,我们需要在项目build.gradle中作如下配置: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569c8ec794e7b.jpg) 具体关于BaseAdapter的使用讲解博客地址如下: * [BaseAdapterHelper的基本使用介绍,让你摆脱狂写一堆Adapter烦恼(二十四)](http://blog.csdn.net/developer_jiangqq/article/details/49724999) * [BaseAdapterHelper详解源码分析,让你摆脱狂写一堆Adapter烦恼(二十五)](http://blog.csdn.net/developer_jiangqq/article/details/49745257) 4.正式运行效果如下: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569c8ec7c69cf.jpg) 5.因为这边底层需要ViewDragHelper类,所以大家在使用的时候需要导入V4包的,不过我这边直接把ViewGragHelper类的源代码复制到项目中了。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569c8ec827d0b.jpg) ## (六).DragLayout源代码带注释 上面主要分析DragLayout的具体实现,不过我这边也贴一下DragLayout带有注释的全部源代码让大家可以更好的了解DragLayout的具体实现代码: ~~~ /** *使用ViewRragHelper实现侧滑效果功能 */ publicclass DragLayout extends FrameLayout { private boolean isShowShadow = true; //手势处理类 private GestureDetectorCompat gestureDetector; //视图拖拽移动帮助类 private ViewDragHelper dragHelper; //滑动监听器 private DragListener dragListener; //水平拖拽的距离 private int range; //宽度 private int width; //高度 private int height; //main视图距离在ViewGroup距离左边的距离 private int mainLeft; private Context context; private ImageView iv_shadow; //左侧布局 private RelativeLayout vg_left; //右侧(主界面布局) private CustomRelativeLayout vg_main; //页面状态 默认为关闭 private Status status = Status.Close; public DragLayout(Context context) { this(context, null); } public DragLayout(Context context,AttributeSet attrs) { this(context, attrs, 0); this.context = context; } public DragLayout(Context context,AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); gestureDetector = new GestureDetectorCompat(context, new YScrollDetector()); dragHelper =ViewDragHelper.create(this, dragHelperCallback); } class YScrollDetector extends SimpleOnGestureListener { @Override public boolean onScroll(MotionEvent e1,MotionEvent e2, float dx, float dy) { return Math.abs(dy) <=Math.abs(dx); } } /** * 实现子View的拖拽滑动,实现Callback当中相关的方法 */ private ViewDragHelper.Callback dragHelperCallback = new ViewDragHelper.Callback() { /** * 水平方向移动 * @param child Child view beingdragged * @param left Attempted motion alongthe X axis * @param dx Proposed change inposition for left * @return */ @Override public int clampViewPositionHorizontal(View child, int left, int dx) { if (mainLeft + dx < 0) { return 0; } else if (mainLeft + dx >range) { return range; } else { return left; } } /** * 拦截所有的子View * @param child Child the user isattempting to capture * @param pointerId ID of the pointerattempting the capture * @return */ @Override public boolean tryCaptureView(View child, int pointerId) { return true; } /** * 设置水平方向滑动的最远距离 *@param child Child view to check 屏幕宽度 * @return */ @Override public int getViewHorizontalDragRange(View child) { return width; } /** * 当拖拽的子View,手势释放的时候回调的方法, 然后根据左滑或者右滑的距离进行判断打开或者关闭 * @param releasedChild * @param xvel * @param yvel */ @Override public void onViewReleased(View releasedChild, float xvel, float yvel) { super.onViewReleased(releasedChild,xvel, yvel); if (xvel > 0) { open(); } else if (xvel < 0) { close(); } else if (releasedChild == vg_main&& mainLeft > range * 0.3) { open(); } else if (releasedChild == vg_left&& mainLeft > range * 0.7) { open(); } else { close(); } } /** * 子View被拖拽 移动的时候回调的方法 * @param changedView View whoseposition changed * @param left New X coordinate of theleft edge of the view * @param top New Y coordinate of thetop edge of the view * @param dx Change in X position fromthe last call * @param dy Change in Y position fromthe last call */ @Override public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) { if (changedView == vg_main) { mainLeft = left; } else { mainLeft = mainLeft + left; } if (mainLeft < 0) { mainLeft = 0; } else if (mainLeft > range) { mainLeft = range; } if (isShowShadow) { iv_shadow.layout(mainLeft, 0,mainLeft + width, height); } if (changedView == vg_left) { vg_left.layout(0, 0, width,height); vg_main.layout(mainLeft, 0,mainLeft + width, height); } dispatchDragEvent(mainLeft); } }; /** * 滑动相关回调接口 */ public interface DragListener { //界面打开 public void onOpen(); //界面关闭 public void onClose(); //界面滑动过程中 public void onDrag(float percent); } public void setDragListener(DragListener dragListener) { this.dragListener = dragListener; } /** * 布局加载完成回调 * 做一些初始化的操作 */ @Override protected void onFinishInflate() { super.onFinishInflate(); if (isShowShadow) { iv_shadow = new ImageView(context); iv_shadow.setImageResource(R.mipmap.shadow); LayoutParams lp = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); addView(iv_shadow, 1, lp); } //左侧界面 vg_left = (RelativeLayout)getChildAt(0); //右侧(主)界面 vg_main = (CustomRelativeLayout)getChildAt(isShowShadow ? 2 : 1); vg_main.setDragLayout(this); vg_left.setClickable(true); vg_main.setClickable(true); } public ViewGroup getVg_main() { return vg_main; } public ViewGroup getVg_left() { return vg_left; } @Override protected void onSizeChanged(int w, int h,int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); width = vg_left.getMeasuredWidth(); height = vg_left.getMeasuredHeight(); //可以水平拖拽滑动的距离 一共为屏幕宽度的80% range = (int) (width * 0.8f); } /** * 调用进行left和main 视图进行位置布局 * @param changed * @param left * @param top * @param right * @param bottom */ @Override protected void onLayout(boolean changed,int left, int top, int right, int bottom) { vg_left.layout(0, 0, width, height); vg_main.layout(mainLeft, 0, mainLeft +width, height); } /** * 拦截触摸事件 * @param ev * @return */ @Override public boolean onInterceptTouchEvent(MotionEvent ev) { returndragHelper.shouldInterceptTouchEvent(ev) &&gestureDetector.onTouchEvent(ev); } /** * 将拦截的到事件给ViewDragHelper进行处理 * @param e * @return */ @Override public boolean onTouchEvent(MotionEvent e){ try { dragHelper.processTouchEvent(e); } catch (Exception ex) { ex.printStackTrace(); } return false; } /** * 进行处理拖拽事件 * @param mainLeft */ private void dispatchDragEvent(intmainLeft) { if (dragListener == null) { return; } float percent = mainLeft / (float)range; //滑动动画效果 animateView(percent); //进行回调滑动的百分比 dragListener.onDrag(percent); Status lastStatus = status; if (lastStatus != getStatus()&& status == Status.Close) { dragListener.onClose(); } else if (lastStatus != getStatus()&& status == Status.Open) { dragListener.onOpen(); } } /** * 根据滑动的距离的比例,进行平移动画 * @param percent */ private void animateView(float percent) { float f1 = 1 - percent * 0.5f; ViewHelper.setTranslationX(vg_left,-vg_left.getWidth() / 2.5f + vg_left.getWidth() / 2.5f * percent); if (isShowShadow) { //阴影效果视图大小进行缩放 ViewHelper.setScaleX(iv_shadow, f1* 1.2f * (1 - percent * 0.10f)); ViewHelper.setScaleY(iv_shadow, f1* 1.85f * (1 - percent * 0.10f)); } } /** * 有加速度,当我们停止滑动的时候,该不会立即停止动画效果 */ @Override public void computeScroll() { if (dragHelper.continueSettling(true)){ ViewCompat.postInvalidateOnAnimation(this); } } /** * 页面状态(滑动,打开,关闭) */ public enum Status { Drag, Open, Close } /** * 页面状态设置 * @return */ public Status getStatus() { if (mainLeft == 0) { status = Status.Close; } else if (mainLeft == range) { status = Status.Open; } else { status = Status.Drag; } return status; } public void open() { open(true); } public void open(boolean animate) { if (animate) { //继续滑动 if(dragHelper.smoothSlideViewTo(vg_main, range, 0)) { ViewCompat.postInvalidateOnAnimation(this); } } else { vg_main.layout(range, 0, range * 2,height); dispatchDragEvent(range); } } public void close() { close(true); } public void close(boolean animate) { if (animate) { //继续滑动 if(dragHelper.smoothSlideViewTo(vg_main, 0, 0)) { ViewCompat.postInvalidateOnAnimation(this); } } else { vg_main.layout(0, 0, width,height); dispatchDragEvent(0); } } } ~~~ ## (七).最后总结 今天我们实现打造QQ最新版本QQ6.X效果,同时里边用到了ViewDragHelper,BaseAdapterHelper的运用,具体该知识点的使用方法,我已经在我的博客中更新讲解的文章,欢迎大家查看。 本次具体实例注释过的全部代码已经上传到Github项目中了。同时欢迎大家去Github站点进行clone或者下载浏览: [https://github.com/jiangqqlmj/DragHelper4QQ](https://github.com/jiangqqlmj/DragHelper4QQ) 同时欢迎大家star和fork整个开源快速开发框架项目~ 本例所用其他知识点博文地址如下: * [神器ViewDragHelper完全解析,妈妈再也不担心我自定义ViewGroup滑动View操作啦~(三十三)](http://blog.csdn.net/developer_jiangqq/article/details/50033453) * [神器ViewDragHelper完全解析之详解实现QQ5.X侧滑酷炫效果(三十四)](http://blog.csdn.net/developer_jiangqq/article/details/50043159) * [BaseAdapterHelper的基本使用介绍,让你摆脱狂写一堆Adapter烦恼(二十四)](http://blog.csdn.net/developer_jiangqq/article/details/49724999) * [BaseAdapterHelper详解源码分析,让你摆脱狂写一堆Adapter烦恼(二十五)](http://blog.csdn.net/developer_jiangqq/article/details/49745257) 特别致谢DragLayout组件开发者:[https://github.com/BlueMor/DragLayout](https://github.com/BlueMor/DragLayout)
';

Android Design支持库TabLayout打造仿网易新闻Tab标签效果(三十七)

最后更新于:2022-04-01 07:13:42

## (一).前言: 仿36Kr客户端开发过程中,因为他们网站上面的新闻文章分类比较多,所以我这边还是打算模仿网易新闻APP的主界面新闻标签Tab以及页面滑动效果来进行实现。要实现的顶部的Tab标签的效果有很多方法例如采用开源项目ViewPagerIndicator中的TabPageIndicator就可以实现。不过我们今天不讲ViewPagerIndicator,我们来讲一下Google今年发布的TabLayout组件。在2015年的Google大会上,google发布的新的Android SupportDesign库,里面也包含了几个新的控件,那么TabLayout就是其中一个。使用该组件我们可以很轻松的实现TabPageIndicator效果,并且该为官方的,可以向下兼容很多版本而且可以更加统一Material Design效果。不过查看TabLayout的源码发现该组件也是继承自HorizontalScrollView实现。 [使用HorizontalScrollView打造仿网易tab标签效果,点击进入...](http://blog.csdn.net/developer_jiangqq/article/details/50145759) 本例子具体代码已经上传到下面的项目中,欢迎各位去star和fork一下。          FastDev4Android框架项目地址:[https://github.com/jiangqqlmj/FastDev4Android](https://github.com/jiangqqlmj/FastDev4Android) ## (二).使用方式:    2.1.我这边的项目采用Android Studio进行开发的,所以首先要引入TabLayout的依赖库,如下: ~~~ compile'com.android.support:design:23.1.1' compile'com.android.support:appcompat-v7:23.1.1' ~~~ ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569c8ec6e72fc.jpg) ## (三).效果实现: 我们现在需要仿照网易新闻(36Kr客户端)的Tab标签切换以及底部新闻页面切换的效果。Tab标签实现采用TabLayout组件,底部页面切换采用Fragment+ViewPager+FragmentStatePagerAdapter实现。 [注]:承载Fragment的Activity这边就不讲解了,具体可以看上一篇文章和本项目的代码即可。那我们直接从Fragment开始讲起。 3.1.首先我们需要给Fragment创建一个布局文件如下: ~~~ <?xmlversion="1.0" encoding="utf-8"?> <LinearLayoutxmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:orientation="vertical"android:layout_width="match_parent" android:layout_height="match_parent"> <!--顶部tab标签容器--> <android.support.design.widget.TabLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:id="@+id/tab_layout" android:background="@color/white" app:tabIndicatorColor="@color/color_selected" app:tabSelectedTextColor="@color/color_selected" app:tabTextColor="@color/color_unselected" ></android.support.design.widget.TabLayout> <android.support.v4.view.ViewPager android:id="@+id/info_viewpager" android:layout_width="fill_parent" android:layout_height="fill_parent" /> </LinearLayout> ~~~ 该布局主要为两个控件:第一部分为TableLayout组件该为Tab标签的容器,第二部分为ViewPager组件主要用于显示若干个Fragment进行页面切换。大家在TabLayout组件中应该已经注意到了三个自定义属性如下: ~~~ app:tabIndicatorColor="@color/white" // 下方滚动的下划线颜色 app:tabSelectedTextColor="@color/gray" // tab被选中后,文字的颜色 app:tabTextColor="@color/white" // tab默认的文字颜色 ~~~ 这三个自定义属性是TabLayout提供的,我们可以任意修改标题颜色以及指示器的颜色,除此之外还提供了以下一些风格设置方法: * tabMaxWidth * tabIndicatorColor * tabIndicatorHeight * tabPaddingStart * tabPaddingEnd * tabBackground * tabTextAppearance * tabSelectedTextColor 3.2.接着是是CNKFixedPagerAdapter,该为ViewPager的自定义适配器,在ViewPager中的每一项采用Fragment实现,所以传入了得Fragment的页面的集合,同时还定义了Tab显示标题的数组。 ~~~ package com.chinaztt.fda.adapter; import android.support.v4.app.Fragment; import android.support.v4.app.FragmentManager; import android.support.v4.app.FragmentStatePagerAdapter; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; import android.widget.TextView; import com.chinaztt.fda.application.FDApplication; import com.chinaztt.fda.ui.R; import java.util.List; /** * 当前类注释:Fragment,Viewpager的自定义适配器 * 项目名:FastDev4Android * 包名:com.chinaztt.fda.adapter * 作者:江清清 on 15/12/2 10:08 * 邮箱:jiangqqlmj@163.com * QQ: 781931404 * 公司:江苏中天科技软件技术有限公司 */ public class CNKFixedPagerAdapter extends FragmentStatePagerAdapter { private String[] titles; private LayoutInflater mInflater; public void setTitles(String[] titles) { this.titles = titles; } private List<Fragment> fragments; public CNKFixedPagerAdapter(FragmentManager fm) { super(fm); } @Override public Fragment getItem(int position) { return this.fragments.get(position); } @Override public int getCount() { return this.fragments.size(); } @Override public Object instantiateItem(ViewGroup container, int position) { Fragment fragment=null; try { fragment=(Fragment)super.instantiateItem(container,position); }catch (Exception e){ } return fragment; } @Override public void destroyItem(ViewGroupcontainer, int position, Object object) { } //此方法用来显示tab上的名字 @Override public CharSequence getPageTitle(intposition) { return titles[position %titles.length]; } public List<Fragment> getFragments(){ return fragments; } public void setFragments(List<Fragment> fragments) { this.fragments = fragments; } } ~~~ 该Adapter中我们重写了getPageTitle()方法,用来显示Tab的标题。 3.3.下面就是具体的Fragment(TabInfoFragment)了,该Fragment中,我们初始化ViewPager,TabLayout以及自定义器适配器,Fragment页面和显示的Tab标题。 最终我们把适配器和ViewPager进行绑定,TabLayout和ViewPager进行绑定即可。 ~~~ public class TabInfoFragment extends Fragment { private String[]titles=new String[]{"全部","氪TV","O2O","新硬件","Fun!!","企业服务","Fit&Health","在线教育","互联网金融","大公司","专栏","新产品"}; private View mView; private TabLayout tab_layout; private ViewPager info_viewpager; private List<Fragment> fragments; private CNKFixedPagerAdapter mPagerAdater; @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { if(mView==null){ mView=inflater.inflate(R.layout.tab_info_fragment_layout,container,false); initViews(); initValidata(); } return mView; } private void initViews(){ tab_layout=(TabLayout)mView.findViewById(R.id.tab_layout); info_viewpager=(ViewPager)mView.findViewById(R.id.info_viewpager); } private void initValidata(){ fragments=new ArrayList<>(); for(int i=0;i<12;i++){ OneFragment oneFragment=new OneFragment(); Bundle bundle=new Bundle(); bundle.putString("extra",titles[i]); oneFragment.setArguments(bundle); fragments.add(oneFragment); } //创建Fragment的 ViewPager 自定义适配器 mPagerAdater=new CNKFixedPagerAdapter(getChildFragmentManager()); //设置显示的标题 mPagerAdater.setTitles(titles); //设置需要进行滑动的页面Fragment mPagerAdater.setFragments(fragments); info_viewpager.setAdapter(mPagerAdater); tab_layout.setupWithViewPager(info_viewpager); //设置Tablayout //设置TabLayout模式 -该使用Tab数量比较多的情况 tab_layout.setTabMode(TabLayout.MODE_SCROLLABLE); } } ~~~ 相信大家已经看到该代码的最后有一句: ~~~ tab_layout.setTabMode(TabLayout.MODE_SCROLLABLE); ~~~ 该用来设置Tablayout的模式,除了上面的默认之外还有一个模式如下: ~~~ tab_layout.setTabMode(TabLayout.MODE_FIXED); ~~~  两者的区别如下,如果Tab数量比较多的情况下,最少用上面那个,Tab标签可以左右滑动显示。 3.4.运行效果如下: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569c8ec7110d4.jpg) ## (四).Tab升级版改造: 上面的效果是属于基础版本直接Tab显示标题就结束了,有时候不会达到我们的要求Tab标签上面显示图标。那么现在我们对其进行改造一下可以让TabLayout打造的Tab既能显示图标又能显示文字标题信息。该主要采用Tab的以下方法实现: ~~~ tab.setCustomView(view); tab添加的自定义布局 ~~~ 4.1.首先对于Tab Item每一项我们需要定义一个布局文件: ~~~ <?xmlversion="1.0" encoding="utf-8"?> <LinearLayoutxmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="vertical" > <ImageView android:src="@mipmap/ic_launcher" android:layout_width="15dp" android:layout_height="15dp" android:id="@+id/imageView" android:layout_gravity="center_horizontal" /> <TextView android:text="Item 01" android:layout_width="wrap_content" android:layout_height="wrap_content" android:id="@+id/textView" android:layout_gravity="center_horizontal" /> </LinearLayout> ~~~ 该布局文件很简单,上下布局图标和标题 4.2.然后我们需要在CNKFixedPagerAdapter中进行以下修改:先前我们是通过重写public CharSequence getPageTitle(int position)该方法来显示标题的,那么现在我们需要删掉或者注释掉该方法,然后新增一个getTabView()方法来实时创建Tab Item的布局然后绑定数据: ~~~ /** * 添加getTabView的方法,来进行自定义Tab的布局View * @param position * @return */ public View getTabView(int position){ mInflater=LayoutInflater.from(FDApplication.getInstance()); Viewview=mInflater.inflate(R.layout.tab_item_layout,null); TextView tv= (TextView)view.findViewById(R.id.textView); tv.setText(titles[position]); ImageView img = (ImageView)view.findViewById(R.id.imageView); img.setImageResource(R.mipmap.ic_launcher); return view; } ~~~ 4.3.这些步骤做完了之后,在主Fragment(TabInfoFragment)中对Tablayout中每一项Tab作如下设置即可:获取到每一项Tab,然后给该Tab设置自定义布局调用tab.setCustomView()方法。 ~~~ //设置自定义Tab--加入图标的demo for(int i=0;i<12;i++){ TabLayout.Tab tab =tab_layout.getTabAt(i); tab.setCustomView(mPagerAdater.getTabView(i)); } ~~~  4.4.运行结果如下: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569c8ec73a84e.jpg) ## (五).最后总结 今天我们通过Android  Design支持库TabLayout组件实现了仿照网易新闻客户端首页的页面滑动和顶部Tab效果。希望对大家有所帮助使用以前的方式虽然也可以显示这样的效果,不过技术在发展嘛,紧跟时代潮流吧~嘿嘿。 本次实例代码因为比较多,重点核心代码已经贴出来。不过实例注释过的全部代码已经上传到Github项目中了。同时欢迎大家去Github站点进行clone或者fork浏览整个开源快速开发框架项目~ [https://github.com/jiangqqlmj/FastDev4Android](https://github.com/jiangqqlmj/FastDev4Android)
';

HorizontalScrollView,Fragment,FragmentStatePagerAdapter打造网易新闻Tab及滑动页面效果(三十六)

最后更新于:2022-04-01 07:13:40

## (一).前言: 仿36Kr客户端开发过程中,因为他们网站上面的新闻文章分类比较多,所以我这边还是打算模仿网易新闻APP的主界面新闻标签Tab以及页面滑动效果来进行实现。要实现的顶部的Tab标签的效果有很多方法例如采用开源项目ViewPagerIndicator中的TabPageIndicator就可以实现,不过查看了源码发现该控件其实就是继承自HorizontalScrollView自定义出来的。那既然这样我这边就准备带着大家直接使用HorizontalScrollView来实现顶部tab标签效果。底部的页面滑动直接采用Fragment+ViewPager+FragmentStatePagerAdapter实现即可。后面我也会更新一篇直接使用ViewPagerIndicator开源控件实现tab标签效果的文章,敬请期待~ 本例子具体代码已经上传到下面的项目中,欢迎各位去star和fork一下。 FastDev4Android框架项目地址:[https://github.com/jiangqqlmj/FastDev4Android](https://github.com/jiangqqlmj/FastDev4Android) ## (二).实现原理: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569c8ec65ef12.jpg) 上面我这边直接贴了36Kr官方APP的主界面顶部Tab标签,我这边直接模仿这个做。首先看上面截图红色框起来的部分,这边的Tab是可以横向滑动的那可以采用HorizontalScrollView控件实现,并且里边的每一项Tab Item都是可以进行添加和点击。我们直接往HorizontalScrollView addView子控件即可。Tab下面是若干个新闻文章列表的页面且可以进行左右滑动可以采用ViewPager实现,每个页面采用Fragment实现。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569c8ec678eab.jpg) 上图是具体控件的分布和嵌套,下面我们来看一下具体实现: ## (三).具体实现: 3.1.首先我们需要有一个继承自FragmentActivity的MainInfoActivity,该用来承载Fragment,该Activity中实现基本没有啥代码,就不贴详细到时候去FastDev4Android项目中下载即可,这边我们看一下Activity的布局文件: ~~~ <?xmlversion="1.0" encoding="utf-8"?> <LinearLayoutxmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:orientation="vertical"android:layout_width="match_parent" android:layout_height="match_parent" > <includelayout="@layout/common_top_bar_layout"/> <fragment android:id="@+id/info_fragment" class="com.chinaztt.fda.fragment.InfoFragment" android:layout_width="fill_parent" android:layout_height="fill_parent" tools:layout="@layout/info_fragment_layout" /> </LinearLayout> ~~~ 该布局文件中直接把承载的InfoFragment写在里边了,当我们Activity加载的时候该Fragment也被加载了。 3.2.接下来就是InfoFragment了,让我们首先看下我这边定义的布局文件: ~~~ <?xmlversion="1.0" encoding="utf-8"?> <LinearLayoutxmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical"android:layout_width="match_parent" android:layout_height="match_parent" android:background="@color/white"> <!--横向滑动的容器--> <HorizontalScrollView android:id="@+id/horizontal_info" android:layout_width="fill_parent" android:layout_height="wrap_content" android:scrollbars="none"> <!--装入每一个Tab项容器--> <LinearLayout android:id="@+id/linearlayout_container" android:layout_width="fill_parent" android:layout_height="49dp" android:orientation="horizontal"> </LinearLayout> </HorizontalScrollView> <android.support.v4.view.ViewPager android:id="@+id/info_viewpager" android:layout_width="fill_parent" android:layout_height="fill_parent"/> </LinearLayout> ~~~            该布局中主要分为两部分,第一部分就是HorizontalScrollView控件该用来实现横向滑动,为标签Tab的容器,我们可以往里边动态的添加TabItem。第二部分为ViewPager控件该用来实现页面的左右滑动,其中每一项Item为Fragment。以上关键控件已经定义好了,下面就是需要在InfoFragment中实现Tab效果了。 首先定义和初始化控件: ~~~ /** * 当前选择的分类 */ private int mCurClassIndex=0; /** * 选择的分类字体颜色 */ private int mColorSelected; /** * 非选择的分类字体颜色 */ private int mColorUnSelected; /** * 水平滚动的Tab容器 */ private HorizontalScrollView mScrollBar; /** * 分类导航的容器 */ private ViewGroup mClassContainer; /** * 水平滚动X */ private int mScrollX=0; mScrollBar=(HorizontalScrollView)mView.findViewById(R.id.horizontal_info); mClassContainer=(ViewGroup)mView.findViewById(R.id.linearlayout_container); ~~~ 对于Tab Item的动态添加使用下面写得方法addScrollView(): ~~~ /** * 动态添加顶部Tab滑动的标签 * @param titles */ private void addScrollView(String[]titles){ LayoutInflater mLayoutInflater=LayoutInflater.from(FDApplication.getInstance()); final int count=titles.length; for(int i=0;i<count;i++){ final String title=titles[i]; final Viewview=mLayoutInflater.inflate(R.layout.horizontal_item_layout,null); final LinearLayout linearLayout=(LinearLayout)view.findViewById(R.id.horizontal_linearlayout_type); final ImageView img_type=(ImageView)view.findViewById(R.id.horizontal_img_type); final TextView type_name=(TextView)view.findViewById(R.id.horizontal_tv_type); type_name.setText(title); if(i==mCurClassIndex){ //已经选中 type_name.setTextColor(mColorSelected); img_type.setImageResource(R.drawable.bottom_line_blue); }else { //未选中 type_name.setTextColor(mColorUnSelected); img_type.setImageResource(R.drawable.bottom_line_gray); } final int index=i; //点击顶部Tab标签,动态设置下面的ViewPager页面 view.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { //首先设置当前的Item为正常状态 View currentItem=mClassContainer.getChildAt(mCurClassIndex); ((TextView)(currentItem.findViewById(R.id.horizontal_tv_type))).setTextColor(mColorUnSelected); ((ImageView)(currentItem.findViewById(R.id.horizontal_img_type))).setImageResource(R.drawable.bottom_line_gray); mCurClassIndex=index; //设置点击状态 img_type.setImageResource(R.drawable.bottom_line_blue); type_name.setTextColor(mColorSelected); //跳转到指定的ViewPager info_viewpager.setCurrentItem(mCurClassIndex); } }); mClassContainer.addView(view); } } ~~~ 该方法传入了标签的数组,根据标签的数量进行遍历动态添加,主要步骤如下: * 加载每一项Tab Item的布局,并且获取Item中的相关控件并且设置数据和资源文件 * 判断当前是否选中项,对于选中和未选中设置不同的字体颜色和资源文件 * 给每一项Item添加点击事件,用来切换ViewPager跳转到具体每一项页面(Fragment) * 最终每一项Tab Item加入到容器中 上面我们有讲到,使用Fragment+ViewPager实现页面滑动切换,那我们需要一个页面的自定义适配器了,我这边创建了CNKFixedPagerAdapter该类继承自FragmengStatePagerAdaper具体实现代码如下,比较简单就不详细讲解了: ~~~ public class CNKFixedPagerAdapter extends FragmentStatePagerAdapter { private String[] titles; public void setTitles(String[] titles) { this.titles = titles; } private List<Fragment> fragments; public CNKFixedPagerAdapter(FragmentManager fm) { super(fm); } @Override public Fragment getItem(int position) { return this.fragments.get(position); } @Override public int getCount() { return this.fragments.size(); } @Override public Object instantiateItem(ViewGroup container, int position) { Fragment fragment=null; try { fragment=(Fragment)super.instantiateItem(container,position); }catch (Exception e){ } return fragment; } @Override public void destroyItem(ViewGroup container, int position, Object object) { } public List<Fragment> getFragments(){ return fragments; } public void setFragments(List<Fragment> fragments) { this.fragments = fragments; } } ~~~ 然后我们实例化ViewPager以及自定义适配器和显示的Fragment数据绑定即可: ~~~ fragments=new ArrayList<>(); for(int i=0;i<12;i++){ OneFragment oneFragment=new OneFragment(); Bundle bundle=new Bundle(); bundle.putString("extra",titles[i]); oneFragment.setArguments(bundle); fragments.add(oneFragment); } mPagerAdater=new CNKFixedPagerAdapter(getChildFragmentManager()); mPagerAdater.setTitles(titles); mPagerAdater.setFragments(fragments); info_viewpager.setAdapter(mPagerAdater); ~~~ 最后我们不要忘记有一点是:当我们的ViewPager页面切换的时候我们需要实习改变顶部Tab Item的选中情况以及字体颜色等。所以我们需要给ViewPager添加页面切换监听器OnPageChangeListener,然后在回调的onPageSelected()方法中重新设置一下Tab Item的效果。 ~~~ info_viewpager.setOnPageChangeListener(this); ~~~ ~~~ //下面三个回调方法 分别是在ViewPager进行滑动的时候调用 @Override public void onPageScrolled(int position,float positionOffset, int positionOffsetPixels) { } @Override public void onPageSelected(int position) { //首先设置当前的Item为正常状态 View preView=mClassContainer.getChildAt(mCurClassIndex); ((TextView)(preView.findViewById(R.id.horizontal_tv_type))).setTextColor(mColorUnSelected); ((ImageView)(preView.findViewById(R.id.horizontal_img_type))).setImageResource(R.drawable.bottom_line_gray); mCurClassIndex=position; //设置当前为选中状态 View currentItem=mClassContainer.getChildAt(mCurClassIndex); ((ImageView)(currentItem.findViewById(R.id.horizontal_img_type))).setImageResource(R.drawable.bottom_line_blue); ((TextView)(currentItem.findViewById(R.id.horizontal_tv_type))).setTextColor(mColorSelected); //这边移动的距离 是经过计算粗略得出来的 mScrollX=currentItem.getLeft()-300; Log.d("zttjiangqq","mScrollX:" + mScrollX); mScrollBar.post(new Runnable() { @Override public void run() { mScrollBar.scrollTo(mScrollX,0); } }); } @Override public void onPageScrollStateChanged(int state) { } ~~~ 上面onPageSelected()方法中我们首先设置原先的Item的颜色为正常未选中状态,然后设置当前的位置选中以及字体颜色改变,最后让HorizontalScrollView平移到合适的位置即可: 3.3.以上我们的核心代码已经讲解完成了,下面我们看一下运行效果: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569c8ec69e875.jpg) 3.4.为了大家方便阅读代码,我这边把InfoFragment的全部代码贴出来: ~~~ public class InfoFragment extends Fragment implements ViewPager.OnPageChangeListener{ private View mView; ViewPager info_viewpager; private List<Fragment> fragments; private CNKFixedPagerAdapter mPagerAdater; private String[] titles=new String[]{"全部","氪TV","O2O","新硬件","Fun!!","企业服务","Fit&Health","在线教育","互联网金融","大公司","专栏","新产品"}; /** * 当前选择的分类 */ private int mCurClassIndex=0; /** * 选择的分类字体颜色 */ private int mColorSelected; /** * 非选择的分类字体颜色 */ private int mColorUnSelected; /** * 水平滚动的Tab容器 */ private HorizontalScrollView mScrollBar; /** * 分类导航的容器 */ private ViewGroup mClassContainer; /** * 水平滚动X */ private int mScrollX=0; @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { if(mView==null){ mView=inflater.inflate(R.layout.info_fragment_layout,container,false); initViews(); initValidata(); } return mView; } /** * 初始化布局控件 */ private void initViews(){ info_viewpager=(ViewPager)mView.findViewById(R.id.info_viewpager); mScrollBar=(HorizontalScrollView)mView.findViewById(R.id.horizontal_info); mClassContainer=(ViewGroup)mView.findViewById(R.id.linearlayout_container); } private void initValidata(){ mColorSelected=FDApplication.getInstance().getResources().getColor(R.color.color_selected); mColorUnSelected=FDApplication.getInstance().getResources().getColor(R.color.color_unselected); //添加Tab标签 addScrollView(titles); mScrollBar.post(new Runnable() { @Override public void run() { mScrollBar.scrollTo(mScrollX,0); } }); fragments=new ArrayList<>(); for(int i=0;i<12;i++){ OneFragment oneFragment=new OneFragment(); Bundle bundle=new Bundle(); bundle.putString("extra",titles[i]); oneFragment.setArguments(bundle); fragments.add(oneFragment); } mPagerAdater=new CNKFixedPagerAdapter(getChildFragmentManager()); mPagerAdater.setTitles(titles); mPagerAdater.setFragments(fragments); info_viewpager.setAdapter(mPagerAdater); info_viewpager.setOnPageChangeListener(this); } /** * 动态添加顶部Tab滑动的标签 * @param titles */ private void addScrollView(String[]titles){ LayoutInflater mLayoutInflater=LayoutInflater.from(FDApplication.getInstance()); final int count=titles.length; for(int i=0;i<count;i++){ final String title=titles[i]; final View view=mLayoutInflater.inflate(R.layout.horizontal_item_layout,null); final LinearLayout linearLayout=(LinearLayout)view.findViewById(R.id.horizontal_linearlayout_type); final ImageView img_type=(ImageView)view.findViewById(R.id.horizontal_img_type); final TextView type_name=(TextView)view.findViewById(R.id.horizontal_tv_type); type_name.setText(title); if(i==mCurClassIndex){ //已经选中 type_name.setTextColor(mColorSelected); img_type.setImageResource(R.drawable.bottom_line_blue); }else { //未选中 type_name.setTextColor(mColorUnSelected); img_type.setImageResource(R.drawable.bottom_line_gray); } final int index=i; //点击顶部Tab标签,动态设置下面的ViewPager页面 view.setOnClickListener(newView.OnClickListener() { @Override public void onClick(View v) { //首先设置当前的Item为正常状态 View currentItem=mClassContainer.getChildAt(mCurClassIndex); ((TextView)(currentItem.findViewById(R.id.horizontal_tv_type))).setTextColor(mColorUnSelected); ((ImageView)(currentItem.findViewById(R.id.horizontal_img_type))).setImageResource(R.drawable.bottom_line_gray); mCurClassIndex=index; //设置点击状态 img_type.setImageResource(R.drawable.bottom_line_blue); type_name.setTextColor(mColorSelected); //跳转到指定的ViewPager info_viewpager.setCurrentItem(mCurClassIndex); } }); mClassContainer.addView(view); } } //下面三个回调方法 分别是在ViewPager进行滑动的时候调用 @Override public void onPageScrolled(int position,float positionOffset, int positionOffsetPixels) { } @Override public void onPageSelected(int position) { //首先设置当前的Item为正常状态 View preView=mClassContainer.getChildAt(mCurClassIndex); ((TextView)(preView.findViewById(R.id.horizontal_tv_type))).setTextColor(mColorUnSelected); ((ImageView)(preView.findViewById(R.id.horizontal_img_type))).setImageResource(R.drawable.bottom_line_gray); mCurClassIndex=position; //设置当前为选中状态 View currentItem=mClassContainer.getChildAt(mCurClassIndex); ((ImageView)(currentItem.findViewById(R.id.horizontal_img_type))).setImageResource(R.drawable.bottom_line_blue); ((TextView)(currentItem.findViewById(R.id.horizontal_tv_type))).setTextColor(mColorSelected); //这边移动的距离 是经过计算粗略得出来的 mScrollX=currentItem.getLeft()-300; Log.d("zttjiangqq","mScrollX:" + mScrollX); mScrollBar.post(new Runnable() { @Override public void run() { mScrollBar.scrollTo(mScrollX,0); } }); } @Override public void onPageScrollStateChanged(int state) { } } ~~~ ## (四).最后总结 今天我们通过Fragment+ViewPager+FragmentStatePagerAdapter+HorizontalScrollView实现了仿照网易新闻客户端(或者36Kr)首页的页面滑动和顶部Tab效果。 本次实例代码因为比较多,代码全贴比较浪费篇幅,重点在于讲解思路了。不过实例注释过的全部代码已经上传到Github项目中了。同时欢迎大家去Github站点进行clone或者fork浏览整个开源快速开发框架项目~ [https://github.com/jiangqqlmj/FastDev4Android](https://github.com/jiangqqlmj/FastDev4Android)
';

实例解析之SwipeRefreshLayout+RecyclerView+CardView(三十五)

最后更新于:2022-04-01 07:13:38

## (一).前言: 作为Android L开始,Google更新了新控件RecyclerView和CardView,这两个控件在之前的文章中已经做了详细介绍和使用,同时在前面还对下拉刷新组件SwipeRefreshLayout进行相关讲解。本来该专题不在更新了,正好昨天有一个群友问到了怎么样结合SwipeRefreshLayout,RecyclerView,CardView这三种控件实现表格布局界面并且加入下拉刷新和上拉加载更多的效果,那么今天我们来实现并且一步步的完善Demo。 同时关于RecyclerView,CardView,SwipeRefreshLayout控件的使用讲解如下: * [RecyclerView完全解析,让你从此爱上它(二十八)](http://blog.csdn.net/developer_jiangqq/article/details/49927631) * [RecyclerView完全解析之打造新版类Gallery效果(二十九)](http://blog.csdn.net/developer_jiangqq/article/details/49946589) * [RecyclerView完全解析之结合AA(Android Annotations)注入框架实例(三十)](http://blog.csdn.net/developer_jiangqq/article/details/49967587) * [RecyclerView完全解析之下拉刷新与上拉加载SwipeRefreshLayout(三十一)](http://blog.csdn.net/developer_jiangqq/article/details/49992269) * [CardView完全解析与RecyclerView结合使用(三十二)](http://blog.csdn.net/developer_jiangqq/article/details/50000733) 具体代码已经上传到下面的项目中,欢迎各位去star和fork一下。 FastDev4Android框架项目地址:[https://github.com/jiangqqlmj/FastDev4Android](https://github.com/jiangqqlmj/FastDev4Android) ##(二).需求介绍 以上三种控件结合需要实现的效果如下: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569c8ec2ddaf3.jpg) 分析需要实现的界面的效果,首先是表格布局(GirdView)的列表,并且加入下拉刷新和上拉加载更多效果。这边对于表格中每一项我们可以使用CardView,然后列表这块使用RecyclerView,刷新这块我们采用Android给我们提供的SwipeRefreshLayout控件,下面我们来具体实现以下这个效果: ##(三).实例实现(基础): ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569c8ec2f2781.jpg) 实现的第一种方法,我们可以把界面中的每一项Item布局都采用CardView来实现,那么每一项最终形成一个表格布局(GirdView),主列表采用RecyclerView,那么这边的布局管理器需要采用GridLayoutManger并且每行两列分布即可,通过前面的文章我们知道SwipeRefreshLayout的使用方法在RecyclerView外部套用即可。 3.1.Item View的布局文件如下: ~~~ <?xmlversion="1.0" encoding="utf-8"?> <android.support.v7.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android" xmlns:cardview="http://schemas.android.com/apk/res-auto" android:id="@+id/instance_cardview" android:layout_width="wrap_content" android:layout_height="wrap_content" cardview:cardBackgroundColor="@color/black" > <LinearLayout android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="vertical" android:background="@color/black" android:gravity="center" android:layout_gravity="center_horizontal" > <ImageView android:layout_margin="3dp" android:id="@+id/item_img" android:layout_width="150dp" android:layout_height="150dp" android:scaleType="fitXY" /> <TextView android:textSize="16sp" android:layout_margin="3dp" android:textColor="@color/white" android:id="@+id/item_tv" android:layout_width="wrap_content" android:layout_height="wrap_content" android:maxLines="2" android:lines="2"/> </LinearLayout> </android.support.v7.widget.CardView> ~~~ 该布局主体为一个CardView,在CardView内部为一个图片和标题TextView组成。 3.2.接着创建继承自RecyclerView.Adapter的ComInstanceAdapter适配器即可。查看下面的代码就可以知道,我在里边自定义两个ViewHolder,一个专门承载Item View,另一个是承载列表底部Foot View(用来显示上拉加载更多提示和进度的)。也就是我们会在布局添加刷新的时候会在RecyclerView的底部多添加一个布局View(Foot View)(该具体使用方法我们在前面的文章已经讲过了,如不了解可以查看前文)。并且我们这边已经也为RecyclerView扩展的Item点击事件了。唯一和以前实现代码不一样的为: ~~~ @Override public int getItemCount() { if(mInstanceBeans.size()%2==0){ //偶数 return mInstanceBeans.size()+1; }else{ return mInstanceBeans.size()+2; } } ~~~ ~~~ public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { if(holder instanceof ItemViewHolder){ if(position<mInstanceBeans.size()){ ((ItemViewHolder)holder).item_img.setImageResource(mInstanceBeans.get(position).getImg()); ((ItemViewHolder)holder).item_tv.setText(mInstanceBeans.get(position).getTitle()); holder.itemView.setTag(position); holder.itemView.setClickable(true); }else { ((ItemViewHolder)holder).item_img.setImageResource(R.drawable.moren); ((ItemViewHolder)holder).item_tv.setText(""); holder.itemView.setClickable(false); } }else if(holder instanceof FootViewHolder){ //上拉加载更多布局数据绑定 } } ~~~ 上面数量我进行判断需要绑定的数据是偶数还是奇数,因为我们这边的每一行是两个数据,如果我们的数据是奇数的话,那么这边最后的FootView布局会加在右边了如下显示: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569c8ec31fbb0.jpg) 这样的效果是比较丑的,所以要把FootView移动到底部,奇数情况数据进行加1即可,最后数据绑定的时候控制显示一下。 ComInstanceAdapter完成代码如下: ~~~ public class ComInstanceAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> { private Context mContext; private List<InstanceBean> mInstanceBeans; private LayoutInflater mInflater; //布局新增一项类别 //普通ITEM private static final int ITEM_VIEW=1; //FOOT ITEM private static final int FOOT_VIEW=2; public ComInstanceAdapter(Context context,List<InstanceBean> pInstanceBeans){ this.mContext=context; this.mInstanceBeans=pInstanceBeans; mInflater=LayoutInflater.from(this.mContext); } @Override public RecyclerView.ViewHolder onCreateViewHolder(final ViewGroup parent, int viewType) { if (viewType == ITEM_VIEW) { final View view =mInflater.inflate(R.layout.com_instance_item_layout, parent, false); view.setOnClickListener(newView.OnClickListener() { @Override public void onClick(View v) { if (onItemClickListener !=null) { onItemClickListener.onItemClick(view, (int) view.getTag()); } } }); return new ItemViewHolder(view); } else if (viewType == FOOT_VIEW) { View view =mInflater.inflate(R.layout.instance_load_more_layout, parent, false); return new FootViewHolder(view); } return null; } @Override public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { if(holder instanceof ItemViewHolder){ if(position<mInstanceBeans.size()){ ((ItemViewHolder)holder).item_img.setImageResource(mInstanceBeans.get(position).getImg()); ((ItemViewHolder)holder).item_tv.setText(mInstanceBeans.get(position).getTitle()); holder.itemView.setTag(position); holder.itemView.setClickable(true); }else { ((ItemViewHolder)holder).item_img.setImageResource(R.drawable.moren); ((ItemViewHolder)holder).item_tv.setText(""); holder.itemView.setClickable(false); } }else if(holder instanceofFootViewHolder){ //上拉加载更多布局数据绑定 } } @Override public int getItemViewType(int position) { if (position + 1 == getItemCount()) { return FOOT_VIEW; } else { return ITEM_VIEW; } } @Override public int getItemCount() { if(mInstanceBeans.size()%2==0){ //偶数 return mInstanceBeans.size()+1; }else{ return mInstanceBeans.size()+2; } } public static class ItemViewHolder extends RecyclerView.ViewHolder{ private ImageView item_img; private TextView item_tv; public ItemViewHolder(View itemView) { super(itemView); item_img=(ImageView)itemView.findViewById(R.id.item_img); item_tv=(TextView)itemView.findViewById(R.id.item_tv); } } /** * 底部FootView布局 */ public static class FootViewHolder extends RecyclerView.ViewHolder{ private TextView foot_view_item_tv; public FootViewHolder(View view) { super(view); foot_view_item_tv=(TextView)view.findViewById(R.id.foot_view_item_tv); } } /** * Item 点击监听回调接口 */ public interface OnItemClickListener { void onItemClick(View view,intposition); } private OnItemClickListener onItemClickListener; public OnItemClickListener getOnItemClickListener() { return onItemClickListener; } public void setOnItemClickListener(OnItemClickListener onItemClickListener) { this.onItemClickListener =onItemClickListener; } /** * 进行下拉刷新数据添加 并且刷新UI * @param pInstanceBeans */ public void addRefreshBeans(List<InstanceBean> pInstanceBeans){ List<InstanceBean> temp=new ArrayList<InstanceBean>(); temp.addAll(pInstanceBeans); temp.addAll(mInstanceBeans); mInstanceBeans.removeAll(mInstanceBeans); mInstanceBeans.addAll(temp); notifyDataSetChanged(); } /** * 进行上拉加载更多 并且刷新UI * @param pInstanceBeans */ public void addMoreBeans(List<InstanceBean> pInstanceBeans){ mInstanceBeans.addAll(pInstanceBeans); notifyDataSetChanged(); } } ~~~ 3.3.接下来是布局RecyclerView和SwipeRefreshLayout布局文件了,这个和之前的文章上面的一样,我们在RecyclerView外部嵌套SwipeRefreshLayout组件即可,最后在Activity中获取使用。 com_instance_layout.xml布局完整代码如下: ~~~ <?xmlversion="1.0" encoding="utf-8"?> <LinearLayoutxmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical"android:layout_width="match_parent" android:layout_height="match_parent" android:background="@color/black"> <includelayout="@layout/common_top_bar_layout"/> <android.support.v4.widget.SwipeRefreshLayout android:id="@+id/instance_swiperefreshlayout" android:layout_width="fill_parent" android:layout_height="fill_parent" android:scrollbars="none"> <android.support.v7.widget.RecyclerView android:id="@+id/instance_recycler" android:layout_width="fill_parent" android:layout_height="fill_parent" android:scrollbars="none"/> </android.support.v4.widget.SwipeRefreshLayout> </LinearLayout> ~~~ 3.4.最后Activity中初始化SwipeRefreshLayout控件并且设置背景和刷新进度条的颜色。RecyclerView控件初始化以及设置布局管理器(GirdLayoutManger)和适配器即可。 ComInstanceActivity完整代码如下: ~~~ public class ComInstanceActivity extends BaseActivity { private LinearLayout top_bar_linear_back; private TextView top_bar_title; private RecyclerView instance_recycler; private ComInstanceAdapter adapter; private SwipeRefreshLayout instance_swiperefreshlayout; private int lastVisibleItem; //是否正在加载更多的标志 private boolean isMoreLoading=false; @Override protected void onCreate(BundlesavedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.com_instance_layout); top_bar_linear_back=(LinearLayout)this.findViewById(R.id.top_bar_linear_back); instance_swiperefreshlayout=(SwipeRefreshLayout)this.findViewById(R.id.instance_swiperefreshlayout); //设置刷新时动画的颜色,可以设置4个 instance_swiperefreshlayout.setProgressBackgroundColorSchemeResource(android.R.color.white); instance_swiperefreshlayout.setColorSchemeResources(android.R.color.holo_blue_light, android.R.color.holo_red_light,android.R.color.holo_orange_light, android.R.color.holo_green_light); top_bar_linear_back.setOnClickListener(new CustomOnClickListener()); top_bar_title=(TextView)this.findViewById(R.id.top_bar_title); top_bar_title.setText("综合实例"); instance_recycler=(RecyclerView)this.findViewById(R.id.instance_recycler); final GridLayoutManager gridLayoutManager=new GridLayoutManager(this,2); instance_recycler.setLayoutManager(gridLayoutManager); instance_recycler.setAdapter(adapter =new ComInstanceAdapter(this, InstanceDataUtils.getInstanceBeans())); //添加Item点击监听事件 adapter.setOnItemClickListener(new ComInstanceAdapter.OnItemClickListener() { @Override public void onItemClick(View view,int position) { Toast.makeText(ComInstanceActivity.this,"点击了第"+position+"项",Toast.LENGTH_SHORT).show(); } }); //下拉刷新 instance_swiperefreshlayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() { @Override public void onRefresh() { new Handler().postDelayed(newRunnable() { @Override public void run() { List<InstanceBean> temp=new ArrayList<InstanceBean>(); for(inti=0;i<5;i++){ InstanceBean bean=new InstanceBean("我是杨颖Item"+i,R.drawable.baby); temp.add(bean); } adapter.addRefreshBeans(temp); instance_swiperefreshlayout.setRefreshing(false); Toast.makeText(ComInstanceActivity.this, "更新了五条数据...", Toast.LENGTH_SHORT).show(); } },3500); } }); //上拉加载更多 instance_recycler.setOnScrollListener(new RecyclerView.OnScrollListener() { @Override public void onScrollStateChanged(RecyclerView recyclerView, int newState) { super.onScrollStateChanged(recyclerView, newState); if (newState ==RecyclerView.SCROLL_STATE_IDLE && lastVisibleItem + 1 ==adapter.getItemCount()) { if(!isMoreLoading){ isMoreLoading=true; newHandler().postDelayed(new Runnable() { @Override public void run() { List<InstanceBean> temp=new ArrayList<InstanceBean>(); for (int i = 0; i< 5; i++) { InstanceBean bean=new InstanceBean("我是MoreItem"+i,R.drawable.meinv); temp.add(bean); } adapter.addMoreBeans(temp); Toast.makeText(ComInstanceActivity.this, "上拉加载了五条数据...", Toast.LENGTH_SHORT).show(); isMoreLoading=false; } },2000); } } } @Override public void onScrolled(RecyclerView recyclerView, int dx, int dy) { super.onScrolled(recyclerView,dx, dy); lastVisibleItem =gridLayoutManager.findLastVisibleItemPosition(); } }); } class CustomOnClickListener implementsView.OnClickListener{ @Override public void onClick(View v) { ComInstanceActivity.this.finish(); } } } ~~~ 上面的代码中要使用SwipeRefreshLayout实现下拉刷新只要设置OnRefreshListener监听器即可,要实现上拉加载更多给RecyclerView添加OnScrollListener判断是否已经下拉滑动的底部,然后开始加载更多数据。 3.5.运行效果如下: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569c8ec3818bb.jpg) ##(四).实例实现(改进版): 看到上面的效果实现,我们会发现一个问题:FootView虽然在底部了,但是表格一行是两列的,所以FootView就会在底部的最左边了,只会占据一个CardView的空间。但是我们平时的效果应该是FootView是整一行实现的,这样比较美观。OK 下面我们来进行改进一下: 之前RecyclerView我们采用的是GirdLayoutManger布局,这边我们采用LinearLayoutManger垂直方向实现。这样的话每一行为单独的Item了,并且该行Item中我直接放上一个CardView布局,然后在内部添加两个水平布局的LinearLayout。但是这样修改之后Item的点击事件就要进行修改了,我们这边把点击事件添加在每一个LinearLayout布局。 4.1.advance_com_instance_item_layout完成布局代码如下: ~~~ <?xmlversion="1.0" encoding="utf-8"?> <android.support.v7.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android" xmlns:cardview="http://schemas.android.com/apk/res-auto" android:id="@+id/instance_cardview" android:layout_width="fill_parent" android:layout_height="wrap_content" cardview:cardBackgroundColor="@color/black" > <LinearLayout android:layout_width="fill_parent" android:layout_height="wrap_content" android:orientation="horizontal" android:background="@color/black" android:gravity="center" android:layout_gravity="center_horizontal" > <LinearLayout android:id="@+id/leftL" android:layout_width="fill_parent" android:layout_height="wrap_content" android:orientation="vertical" android:layout_weight="1" android:gravity="center"> <ImageView android:src="@drawable/meinv" android:layout_margin="3dp" android:id="@+id/item_img_one" android:layout_width="150dp" android:layout_height="150dp" android:scaleType="fitXY" /> <TextView android:text="古代美女" android:textSize="16sp" android:layout_margin="3dp" android:textColor="@color/white" android:id="@+id/item_tv_one" android:layout_width="wrap_content" android:layout_height="wrap_content" android:maxLines="2" android:lines="2"/> </LinearLayout> <LinearLayout android:id="@+id/rightL" android:layout_width="fill_parent" android:layout_height="wrap_content" android:orientation="vertical" android:layout_weight="1" android:gravity="center"> <ImageView android:src="@drawable/liuyan" android:layout_margin="3dp" android:id="@+id/item_img_two" android:layout_width="150dp" android:layout_height="150dp" android:scaleType="fitXY" /> <TextView android:text="柳岩" android:textSize="16sp" android:layout_margin="3dp" android:textColor="@color/white" android:id="@+id/item_tv_two" android:layout_width="wrap_content" android:layout_height="wrap_content" android:maxLines="2" android:lines="2"/> </LinearLayout> </LinearLayout> </android.support.v7.widget.CardView> ~~~ 我们看到上面的布局中两个LinearLayout分别加上了两个id,用来后面获取控件并且添加点击监听事件。 4.2.然后自定义适配器中的数据绑定方法和之前的有所不同: ~~~ public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { if(holder instanceof ItemViewHolder){ AdvanceInstanceBean advanceInstanceBean=mAdvanceInstanceBeans.get(position); if(advanceInstanceBean!=null){ final List<InstanceBean> instanceBeans=advanceInstanceBean.getInstanceBeans(); if(instanceBeans.size()==2){ ((ItemViewHolder)holder).item_img_one.setImageResource(instanceBeans.get(0).getImg()); ((ItemViewHolder)holder).item_tv_one.setText(instanceBeans.get(0).getTitle()); ((ItemViewHolder)holder).item_img_two.setImageResource(instanceBeans.get(1).getImg()); ((ItemViewHolder)holder).item_tv_two.setText(instanceBeans.get(1).getTitle()); ((ItemViewHolder)holder).leftL.setOnClickListener(new View.OnClickListener() { @Override public voidonClick(View v) { if(onItemClickListener != null) { onItemClickListener.onItemClick(instanceBeans.get(0)); } } }); ((ItemViewHolder)holder).rightL.setOnClickListener(new View.OnClickListener() { @Override public voidonClick(View v) { if(onItemClickListener!=null){ onItemClickListener.onItemClick(instanceBeans.get(1)); } } }); }else { ((ItemViewHolder)holder).item_img_one.setImageResource(instanceBeans.get(0).getImg()); ((ItemViewHolder)holder).item_tv_one.setText(instanceBeans.get(0).getTitle()); ((ItemViewHolder)holder).item_img_two.setImageResource(R.drawable.moren); ((ItemViewHolder)holder).item_tv_two.setText(""); } } }else if(holder instanceof FootViewHolder){ //上拉加载更多布局数据绑定 } } ~~~ 上面的数据绑定代码中,给左右两个布局分别加入了onClick事件,来进行回调点击数据传递。具体回调接口定义如下: ~~~ /** * Item 点击监听回调接口 */ public interface OnItemClickListener { /** * item回调的数据 * @param instanceBean */ void onItemClick(InstanceBean instanceBean); } ~~~ 具体完成代码比较多就不贴了,到时候大家clone一下项目代码: FastDev4Android框架项目地址:[https://github.com/jiangqqlmj/FastDev4Android](https://github.com/jiangqqlmj/FastDev4Android) 4.3.Acitivty中处理初始化设置的代码就不贴了 4.4.运行效果如下: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569c8ec507414.jpg) ## (五).最后总结 今天我们通过SwipeRefreshLayout+RecyclerView+CardView实现的表格布局以及下拉刷新,上拉加载更多的效果。 本次实例代码因为比较多,代码全贴比较浪费篇幅,重点在于讲解思路了。不过实例注释过的全部代码已经上传到Github项目中了。同时欢迎大家去Github站点进行clone或者fork浏览整个开源快速开发框架项目~ [https://github.com/jiangqqlmj/FastDev4Android](https://github.com/jiangqqlmj/FastDev4Android)
';

神器ViewDragHelper完全解析之详解实现QQ5.X侧滑酷炫效果(三十四)

最后更新于:2022-04-01 07:13:36

## (一).前言: 这几天正在更新录制实战项目,整体框架是采用仿照QQ5.X侧滑效果的。那么我们一般的做法就是自定义ViewGroup或者采用开源项目MenuDrawer或者Google提供的控件DrawerLayout等方式来实现。这些的控件的很多效果基本上都是采用实现onInterceptTouchEvent和onTouchEvent这两个方法进行实现,而且都是根据要实现的效果做自定义处理例如:多点触控处理,加速度方面检测以及控制等等。一般这样做作于普通开发人员来讲也是需要很强的程序与逻辑开发能力,幸好Android开发框架给我们提供了一个组件ViewDragHelper。上一讲我们已经就ViewGragHelper的基本使用做了相关讲解[(点击进入)](http://blog.csdn.net/developer_jiangqq/article/details/50033453),今天来分析实现QQ5.X侧滑酷炫效果的实例(开源项目地址[点击进入](https://github.com/BlueMor/DragLayout))。 具体代码已经上传到下面的项目中,欢迎各位去star和fork一下。 [https://github.com/jiangqqlmj/ViewDragHelperTest](https://github.com/jiangqqlmj/ViewDragHelperTest) FastDev4Android框架项目地址:[https://github.com/jiangqqlmj/FastDev4Android](https://github.com/jiangqqlmj/FastDev4Android) ## (二).ViewDragHelper的基本使用    前面我们学习ViewDragHelper的基本使用方法,同时也知道了里边的若干个方法的用途,下面我们还是把基本的使用步骤温习一下。要使用ViewDragHelper实现子View拖拽移动的步骤如下: 1. 创建ViewDragHelper实例(传入Callback) 2. 重写事件拦截处理方法onInterceptTouch和onTouchEvent 3. 实现Callback,实现其中的相关方法tryCaptureView以及水平或者垂直方向移动的距离方法 更加具体分析大家可以看前一篇博客,或者我们今天这边会通过具体实例讲解一下。 ## (三).QQ5.X侧滑效果实现分析:   在正式版本QQ中的侧滑效果如下: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569c8ec1d4e5a.jpg) 观察上面我们可以理解为两个View,一个是底部的相当于左侧功能View,另外一个是上层主功能内容View,我们在上面进行拖拽上层View或者左右滑动的时候,上层和下层的View相应进行滑动以及View大小变化,同时加入相关的动画。当然我们点击上层的View可以进行打开或者关闭侧滑菜单。 ## (四).侧滑效果自定义组件实现 1.首先我们这边集成自FrameLayout创建一个自定义View  DragLayout。内部的定义的一些变量如下(主要包括一些配置类,手势,ViewDragHelper实例,屏幕宽高,拖拽的子视图View等) ~~~ //是否带有阴影效果 private boolean isShowShadow = true; //手势处理类 private GestureDetectorCompat gestureDetector; //视图拖拽移动帮助类 private ViewDragHelper dragHelper; //滑动监听器 private DragListener dragListener; //水平拖拽的距离 private int range; //宽度 private int width; //高度 private int height; //main视图距离在ViewGroup距离左边的距离 private int mainLeft; private Context context; private ImageView iv_shadow; //左侧布局 private RelativeLayout vg_left; //右侧(主界面布局) private CustomRelativeLayout vg_main; ~~~ 然后在内部还定义了一个回调接口主要处理拖拽过程中的一些页面打开,关闭以及滑动中的事件回调:  ~~~ /** * 滑动相关回调接口 */ public interface DragListener { //界面打开 public void onOpen(); //界面关闭 public void onClose(); //界面滑动过程中 public void onDrag(float percent); } ~~~ 2.开始创建ViewDragHelper实例,依然在自定义View DragLayout初始化的时候创建,使用ViewDragHelper的静态方法: ~~~ public DragLayout(Context context,AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); gestureDetector = new GestureDetectorCompat(context, new YScrollDetector()); dragHelper =ViewDragHelper.create(this, dragHelperCallback); } ~~~ 其中create()方法创建的时候传入了一个dragHelperCallBack回调类,将会在第四点中讲到。 3.接着需要重写ViewGroup中事件方法,拦截触摸事件给ViewDragHelper内部进行处理,这样达到拖拽移动子View视图的目的; ~~~ /** * 拦截触摸事件 * @param ev * @return */ @Override public boolean onInterceptTouchEvent(MotionEvent ev) { return dragHelper.shouldInterceptTouchEvent(ev) &&gestureDetector.onTouchEvent(ev); } /** * 将拦截的到事件给ViewDragHelper进行处理 * @param e * @return */ @Override public boolean onTouchEvent(MotionEvent e){ try { dragHelper.processTouchEvent(e); } catch (Exception ex) { ex.printStackTrace(); } return false; } ~~~ 这边我们在onInterceptTouchEvent拦截让事件从父控件往子View中转移,然后在onTouchEvent方法中拦截让ViewDragHelper进行消费处理。 4.开始自定义创建ViewDragHelper.Callback的实例dragHelperCallback分别实现一个抽象方法tryCaptureView以及重写以下若干个方法来实现侧滑功能,下面一个个来看一下。   ~~~ /** * 拦截所有的子View * @param child Child the user isattempting to capture * @param pointerId ID of the pointerattempting the capture * @return */ @Override public boolean tryCaptureView(View child, int pointerId) { return true; } ~~~ 该进行拦截ViewGroup(本例中为:DragLayout)中所有的子View,直接返回true,表示所有的子View都可以进行拖拽移动。 ~~~ /** * 水平方向移动 * @param child Child view beingdragged * @param left Attempted motion alongthe X axis * @param dx Proposed change inposition for left * @return */ @Override public int clampViewPositionHorizontal(View child, int left, int dx) { if (mainLeft + dx < 0) { return 0; } else if (mainLeft + dx >range) { return range; } else { return left; } } ~~~ 实现该方法表示水平方向滑动,同时方法中会进行判断边界值,例如当上面的main view已经向左移动边界之外了,直接返回0,表示向左最左边只能x=0;然后向右移动会判断向右最远距离range,至于range的初始化后边会讲到。除了这两种情况之外,就是直接返回left即可。 ~~~ /** * 设置水平方向滑动的最远距离 * @param child Child view tocheck 屏幕宽度 * @return */ @Override public int getViewHorizontalDragRange(View child) { return width; } ~~~ 该方法有必要实现,因为该方法在Callback内部默认返回0,也就是说,如果的view的click事件为true,那么会出现整个子View没法拖拽移动的情况了。那么这边直接返回left view宽度了,表示水平方向滑动的最远距离了。 ~~~ /** * 当拖拽的子View,手势释放的时候回调的方法, 然后根据左滑或者右滑的距离进行判断打开或者关闭 * @param releasedChild * @param xvel * @param yvel */ @Override public void onViewReleased(View releasedChild, float xvel, float yvel) { super.onViewReleased(releasedChild,xvel, yvel); if (xvel > 0) { open(); } else if (xvel < 0) { close(); } else if (releasedChild == vg_main&& mainLeft > range * 0.3) { open(); } else if (releasedChild == vg_left&& mainLeft > range * 0.7) { open(); } else { close(); } } ~~~ 该方法在拖拽子View移动手指释放的时候被调用,这是会判断移动向左,向右的意图,进行打开或者关闭man view(上层视图)。下面是实现的最后一个方法:onViewPositionChanged ~~~ /** * 子View被拖拽 移动的时候回调的方法 * @param changedView View whoseposition changed * @param left New X coordinate of theleft edge of the view * @param top New Y coordinate of thetop edge of the view * @param dx Change in X position fromthe last call * @param dy Change in Y position fromthe last call */ @Override public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) { if (changedView == vg_main) { mainLeft = left; } else { mainLeft = mainLeft + left; } if (mainLeft < 0) { mainLeft = 0; } else if (mainLeft > range) { mainLeft = range; } if (isShowShadow) { iv_shadow.layout(mainLeft, 0,mainLeft + width, height); } if (changedView == vg_left) { vg_left.layout(0, 0, width,height); vg_main.layout(mainLeft, 0,mainLeft + width, height); } dispatchDragEvent(mainLeft); } }; ~~~ 该方法是在我们进行拖拽移动子View的过程中进行回调,根据移动坐标位置,然后进行重新定义left view和main view。同时调用dispathDragEvent()方法进行拖拽事件相关处理分发同时根据状态来回调接口: ~~~ /** * 进行处理拖拽事件 * @param mainLeft */ private void dispatchDragEvent(intmainLeft) { if (dragListener == null) { return; } float percent = mainLeft / (float)range; //根据滑动的距离的比例,进行带有动画的缩小和放大View animateView(percent); //进行回调滑动的百分比 dragListener.onDrag(percent); Status lastStatus = status; if (lastStatus != getStatus()&& status == Status.Close) { dragListener.onClose(); } else if (lastStatus != getStatus()&& status == Status.Open) { dragListener.onOpen(); } } ~~~ 该方法中有一行代码float percent=mainLeft/(float)range;算到一个百分比后面会用到 5.至于子View布局的获取初始化以及宽高和水平滑动距离的大小设置方法: ~~~ /** * 布局加载完成回调 * 做一些初始化的操作 */ @Override protected void onFinishInflate() { super.onFinishInflate(); if (isShowShadow) { iv_shadow = new ImageView(context); iv_shadow.setImageResource(R.mipmap.shadow); LayoutParams lp = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); addView(iv_shadow, 1, lp); } //左侧界面 vg_left = (RelativeLayout)getChildAt(0); //右侧(主)界面 vg_main = (CustomRelativeLayout)getChildAt(isShowShadow ? 2 : 1); vg_main.setDragLayout(this); vg_left.setClickable(true); vg_main.setClickable(true); } ~~~ 以及控件大小发生变化回调的方法: ~~~ @Override protected void onSizeChanged(int w, int h,int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); width = vg_left.getMeasuredWidth(); height = vg_left.getMeasuredHeight(); //可以水平拖拽滑动的距离 一共为屏幕宽度的60% range = (int) (width * 0.6f); } ~~~ 在该方法中我们可以实时获取宽和高以及拖拽水平距离。 6.上面的所有核心代码都为使用ViewDragHelper实现子控件View拖拽移动的方法,但是根据我们这边侧滑效果还需要实现动画以及滑动过程中View的缩放效果,所以我们这边引入了一个动画开源库:该库在Github中大家可以下载下来使用一下,当然后面我会把这个库的使用单独拿出来写一篇文章讲解一下。敬请期待~ ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569c8ec1f29af.jpg) 然后根据前面算出来的百分比来缩放View视图: ~~~ /** * 根据滑动的距离的比例,进行带有动画的缩小和放大View * @param percent */ private void animateView(float percent) { float f1 = 1 - percent * 0.3f; //vg_main水平方向 根据百分比缩放 ViewHelper.setScaleX(vg_main, f1); //vg_main垂直方向,根据百分比缩放 ViewHelper.setScaleY(vg_main, f1); //沿着水平X轴平移 ViewHelper.setTranslationX(vg_left,-vg_left.getWidth() / 2.3f + vg_left.getWidth() / 2.3f * percent); //vg_left水平方向 根据百分比缩放 ViewHelper.setScaleX(vg_left, 0.5f +0.5f * percent); //vg_left垂直方向 根据百分比缩放 ViewHelper.setScaleY(vg_left, 0.5f +0.5f * percent); //vg_left根据百分比进行设置透明度 ViewHelper.setAlpha(vg_left, percent); if (isShowShadow) { //阴影效果视图大小进行缩放 ViewHelper.setScaleX(iv_shadow, f1* 1.4f * (1 - percent * 0.12f)); ViewHelper.setScaleY(iv_shadow, f1* 1.85f * (1 - percent * 0.12f)); } getBackground().setColorFilter(evaluate(percent, Color.BLACK,Color.TRANSPARENT), Mode.SRC_OVER); } private Integer evaluate(float fraction,Object startValue, Integer endValue) { int startInt = (Integer) startValue; int startA = (startInt >> 24)& 0xff; int startR = (startInt >> 16)& 0xff; int startG = (startInt >> 8)& 0xff; int startB = startInt & 0xff; int endInt = (Integer) endValue; int endA = (endInt >> 24) &0xff; int endR = (endInt >> 16) &0xff; int endG = (endInt >> 8) &0xff; int endB = endInt & 0xff; return (int) ((startA + (int) (fraction* (endA - startA))) << 24) | (int) ((startR + (int)(fraction * (endR - startR))) << 16) | (int) ((startG + (int)(fraction * (endG - startG))) << 8) | (int) ((startB + (int)(fraction * (endB - startB)))); } ~~~ 7.当然除了上面这些还缺少一个效果就是,当我们滑动过程中假如我们手指释放,按照常理来讲view就不会在进行移动了,那么这边我们需要一个加速度当我们释放之后,还能保持一定的速度,该怎么样实现呢?答案就是实现computeScroll()方法。     ~~~ /** * 有加速度,当我们停止滑动的时候,该不会立即停止动画效果 */ @Override public void computeScroll() { if (dragHelper.continueSettling(true)){ ViewCompat.postInvalidateOnAnimation(this); } } ~~~ OK上面关于DragLayout的核心代码就差不多这么多了,下面是使用DragLayout类来实现侧滑效果啦! ## (五).侧滑效果组件使用      1.首先使用的布局文件如下: ~~~ <com.chinaztt.widget.DragLayoutxmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/dl" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@mipmap/ic_main_left_bg" > <RelativeLayout android:layout_width="match_parent" android:layout_height="match_parent" android:paddingBottom="30dp" android:paddingLeft="30dp" android:paddingTop="50dp" > <LinearLayout android:id="@+id/ll1" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal" > <ImageView android:id="@+id/iv_bottom" android:layout_width="70dp" android:layout_height="70dp" android:src="@mipmap/ic_launcher" /> <LinearLayout android:layout_width="wrap_content" android:layout_height="wrap_content" android:gravity="center_vertical" android:layout_gravity="center_vertical" android:orientation="vertical"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_vertical" android:layout_marginLeft="20dp" android:text="名字:jiangqqlmj" android:textColor="#ffffff" android:textSize="18sp" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_vertical" android:layout_marginLeft="20dp" android:text="QQ:781931404" android:textColor="#ffffff" android:textSize="16sp" /> </LinearLayout> </LinearLayout> <TextView android:id="@+id/tv_mail" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentBottom="true" android:text="jiangqqlmj@163.com" android:textColor="#ffffff" android:textSize="15sp"/> <!--中间列表--> <ListView android:id="@+id/lv" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_above="@id/tv_mail" android:layout_below="@id/ll1" android:layout_marginBottom="30dp" android:layout_marginTop="20dp" android:cacheColorHint="#00000000" android:listSelector="@null" android:divider="@null" android:scrollbars="none" android:textColor="#ffffff" /> </RelativeLayout> <com.chinaztt.widget.CustomRelativeLayout android:layout_width="match_parent" android:layout_height="match_parent" android:background="#FFFFFF" > <LinearLayout android:layout_width="fill_parent" android:layout_height="fill_parent" android:orientation="vertical" > <RelativeLayout android:id="@+id/rl_title" android:layout_width="match_parent" android:layout_height="49dp" android:background="#e7abff" android:gravity="bottom" > <RelativeLayout android:layout_width="fill_parent" android:layout_height="49dp" > <ImageView android:id="@+id/iv_icon" android:layout_width="40dp" android:layout_height="40dp" android:layout_centerVertical="true" android:layout_marginLeft="10dp" android:scaleType="fitXY" android:src="@mipmap/ic_launcher" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="ViewDragHelper实例" android:layout_centerInParent="true" android:textColor="#ffffff" android:textSize="20sp" /> </RelativeLayout> </RelativeLayout> <!--中间内容后面放入Fragment--> <FrameLayout android:layout_width="fill_parent" android:layout_height="fill_parent" > <TextView android:id="@+id/iv_noimg" android:layout_gravity="center" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="中间内容容器..." /> </FrameLayout> </LinearLayout> </com.chinaztt.widget.CustomRelativeLayout> </com.chinaztt.widget.DragLayout> ~~~ 该布局文件中父层View就是DragLayout,然后内部有两个RelativeLayout布局,分别充当左侧View(左侧功能)和主Main View。最后在Activity中获取DragLayout控件,添加事件监听器(DragListener)具体代码如下: ~~~ public class MainActivity extends BaseActivity { privateDragLayout dl; privateListView lv; privateTextView tv_noimg; privateImageView iv_icon, iv_bottom; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); setStatusBar(); initDragLayout(); initView(); } private void initDragLayout() { dl= (DragLayout) findViewById(R.id.dl); dl.setDragListener(new DragLayout.DragListener() { //界面打开的时候 @Override public void onOpen() { } //界面关闭的时候 @Override public void onClose() { } //界面滑动的时候 @Override public void onDrag(float percent) { ViewHelper.setAlpha(iv_icon,1 - percent); } }); } private void initView() { iv_icon= (ImageView) findViewById(R.id.iv_icon); iv_bottom= (ImageView) findViewById(R.id.iv_bottom); tv_noimg= (TextView) findViewById(R.id.iv_noimg); lv= (ListView) findViewById(R.id.lv); lv.setAdapter(new ArrayAdapter<String>(MainActivity.this, R.layout.item_text,new String[] { "item 01", "item 01", "item01", "item 01", "item 01", "item 01", "item01", "item 01", "item 01", "item 01", "item01", "item 01", "item 01", "item 01", "item01", "item 01", "item 01", "item01", "item 01", "item 01", "item 01"})); lv.setOnItemClickListener(new OnItemClickListener() { @Override public void onItemClick(AdapterView<?> arg0, View arg1, intposition, long arg3) { Toast.makeText(MainActivity.this,"ClickItem "+position,Toast.LENGTH_SHORT).show(); } }); iv_icon.setOnClickListener(new OnClickListener() { @Override public void onClick(View arg0) { dl.open(); } }); } @Override protectedvoid onResume() { super.onResume(); } } ~~~ 最终运行效果如下: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569c8ec21b964.jpg) 因为这边底层需要ViewDragHelper类,所以大家在使用的时候需要导入V4包的,不过我这边直接把ViewDragHelper类的源代码复制到项目中了。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569c8ec2c7abd.jpg) ## (六).DragLayout源代码带注释    上面主要分析DragLayout的具体实现,不过我这边也贴一下DragLayout带有注释的全部源代码让大家可以更好的了解DragLayout的具体实现代码: ~~~ /** * 使用ViewRragHelper实现侧滑效果功能 */ public class DragLayout extends FrameLayout { private boolean isShowShadow = true; //手势处理类 private GestureDetectorCompat gestureDetector; //视图拖拽移动帮助类 private ViewDragHelper dragHelper; //滑动监听器 private DragListener dragListener; //水平拖拽的距离 private int range; //宽度 private int width; //高度 private int height; //main视图距离在ViewGroup距离左边的距离 private int mainLeft; private Context context; private ImageView iv_shadow; //左侧布局 private RelativeLayout vg_left; //右侧(主界面布局) private CustomRelativeLayout vg_main; //页面状态 默认为关闭 private Status status = Status.Close; public DragLayout(Context context) { this(context, null); } public DragLayout(Context context,AttributeSet attrs) { this(context, attrs, 0); this.context = context; } public DragLayout(Context context,AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); gestureDetector = new GestureDetectorCompat(context, new YScrollDetector()); dragHelper =ViewDragHelper.create(this, dragHelperCallback); } class YScrollDetector extends SimpleOnGestureListener { @Override public boolean onScroll(MotionEvent e1,MotionEvent e2, float dx, float dy) { return Math.abs(dy) <=Math.abs(dx); } } /** * 实现子View的拖拽滑动,实现Callback当中相关的方法 */ private ViewDragHelper.CallbackdragHelperCallback = new ViewDragHelper.Callback() { /** * 水平方向移动 * @param child Child view beingdragged * @param left Attempted motion alongthe X axis * @param dx Proposed change inposition for left * @return */ @Override public int clampViewPositionHorizontal(View child, int left, int dx) { if (mainLeft + dx < 0) { return 0; } else if (mainLeft + dx >range) { return range; } else { return left; } } /** * 拦截所有的子View * @param child Child the user isattempting to capture * @param pointerId ID of the pointerattempting the capture * @return */ @Override public boolean tryCaptureView(View child, int pointerId) { return true; } /** * 设置水平方向滑动的最远距离 * @param child Child view tocheck 屏幕宽度 * @return */ @Override public int getViewHorizontalDragRange(View child) { return width; } /** * 当拖拽的子View,手势释放的时候回调的方法, 然后根据左滑或者右滑的距离进行判断打开或者关闭 * @param releasedChild * @param xvel * @param yvel */ @Override public void onViewReleased(View releasedChild, float xvel, float yvel) { super.onViewReleased(releasedChild,xvel, yvel); if (xvel > 0) { open(); } else if (xvel < 0) { close(); } else if (releasedChild == vg_main&& mainLeft > range * 0.3) { open(); } else if (releasedChild == vg_left&& mainLeft > range * 0.7) { open(); } else { close(); } } /** * 子View被拖拽 移动的时候回调的方法 * @param changedView View whoseposition changed * @param left New X coordinate of theleft edge of the view * @param top New Y coordinate of thetop edge of the view * @param dx Change in X position fromthe last call * @param dy Change in Y position fromthe last call */ @Override public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) { if (changedView == vg_main) { mainLeft = left; } else { mainLeft = mainLeft + left; } if (mainLeft < 0) { mainLeft = 0; } else if (mainLeft > range) { mainLeft = range; } if (isShowShadow) { iv_shadow.layout(mainLeft, 0,mainLeft + width, height); } if (changedView == vg_left) { vg_left.layout(0, 0, width,height); vg_main.layout(mainLeft, 0,mainLeft + width, height); } dispatchDragEvent(mainLeft); } }; /** * 滑动相关回调接口 */ public interface DragListener { //界面打开 public void onOpen(); //界面关闭 public void onClose(); //界面滑动过程中 public void onDrag(float percent); } public void setDragListener(DragListener dragListener) { this.dragListener = dragListener; } /** * 布局加载完成回调 * 做一些初始化的操作 */ @Override protected void onFinishInflate() { super.onFinishInflate(); if (isShowShadow) { iv_shadow = new ImageView(context); iv_shadow.setImageResource(R.mipmap.shadow); LayoutParams lp = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); addView(iv_shadow, 1, lp); } //左侧界面 vg_left = (RelativeLayout)getChildAt(0); //右侧(主)界面 vg_main = (CustomRelativeLayout)getChildAt(isShowShadow ? 2 : 1); vg_main.setDragLayout(this); vg_left.setClickable(true); vg_main.setClickable(true); } public ViewGroup getVg_main() { return vg_main; } public ViewGroup getVg_left() { return vg_left; } @Override protected void onSizeChanged(int w, int h,int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); width = vg_left.getMeasuredWidth(); height = vg_left.getMeasuredHeight(); //可以水平拖拽滑动的距离 一共为屏幕宽度的60% range = (int) (width * 0.6f); } /** * 调用进行left和main 视图进行位置布局 * @param changed * @param left * @param top * @param right * @param bottom */ @Override protected void onLayout(boolean changed,int left, int top, int right, int bottom) { vg_left.layout(0, 0, width, height); vg_main.layout(mainLeft, 0, mainLeft +width, height); } /** * 拦截触摸事件 * @param ev * @return */ @Override public boolean onInterceptTouchEvent(MotionEvent ev) { returndragHelper.shouldInterceptTouchEvent(ev) &&gestureDetector.onTouchEvent(ev); } /** * 将拦截的到事件给ViewDragHelper进行处理 * @param e * @return */ @Override public boolean onTouchEvent(MotionEvent e){ try { dragHelper.processTouchEvent(e); } catch (Exception ex) { ex.printStackTrace(); } return false; } /** * 进行处理拖拽事件 * @param mainLeft */ private void dispatchDragEvent(int mainLeft) { if (dragListener == null) { return; } float percent = mainLeft / (float)range; //根据滑动的距离的比例,进行带有动画的缩小和放大View animateView(percent); //进行回调滑动的百分比 dragListener.onDrag(percent); Status lastStatus = status; if (lastStatus != getStatus()&& status == Status.Close) { dragListener.onClose(); } else if (lastStatus != getStatus()&& status == Status.Open) { dragListener.onOpen(); } } /** * 根据滑动的距离的比例,进行带有动画的缩小和放大View * @param percent */ private void animateView(float percent) { float f1 = 1 - percent * 0.3f; //vg_main水平方向 根据百分比缩放 ViewHelper.setScaleX(vg_main, f1); //vg_main垂直方向,根据百分比缩放 ViewHelper.setScaleY(vg_main, f1); //沿着水平X轴平移 ViewHelper.setTranslationX(vg_left,-vg_left.getWidth() / 2.3f + vg_left.getWidth() / 2.3f * percent); //vg_left水平方向 根据百分比缩放 ViewHelper.setScaleX(vg_left, 0.5f +0.5f * percent); //vg_left垂直方向 根据百分比缩放 ViewHelper.setScaleY(vg_left, 0.5f +0.5f * percent); //vg_left根据百分比进行设置透明度 ViewHelper.setAlpha(vg_left, percent); if (isShowShadow) { //阴影效果视图大小进行缩放 ViewHelper.setScaleX(iv_shadow, f1* 1.4f * (1 - percent * 0.12f)); ViewHelper.setScaleY(iv_shadow, f1* 1.85f * (1 - percent * 0.12f)); } getBackground().setColorFilter(evaluate(percent, Color.BLACK,Color.TRANSPARENT), Mode.SRC_OVER); } private Integer evaluate(float fraction,Object startValue, Integer endValue) { int startInt = (Integer) startValue; int startA = (startInt >> 24)& 0xff; int startR = (startInt >> 16)& 0xff; int startG = (startInt >> 8)& 0xff; int startB = startInt & 0xff; int endInt = (Integer) endValue; int endA = (endInt >> 24) &0xff; int endR = (endInt >> 16) &0xff; int endG = (endInt >> 8) &0xff; int endB = endInt & 0xff; return (int) ((startA + (int) (fraction* (endA - startA))) << 24) | (int) ((startR + (int)(fraction * (endR - startR))) << 16) | (int) ((startG + (int)(fraction * (endG - startG))) << 8) | (int) ((startB + (int)(fraction * (endB - startB)))); } /** * 有加速度,当我们停止滑动的时候,该不会立即停止动画效果 */ @Override public void computeScroll() { if (dragHelper.continueSettling(true)){ ViewCompat.postInvalidateOnAnimation(this); } } /** * 页面状态(滑动,打开,关闭) */ public enum Status { Drag, Open, Close } /** * 页面状态设置 * @return */ public Status getStatus() { if (mainLeft == 0) { status = Status.Close; } else if (mainLeft == range) { status = Status.Open; } else { status = Status.Drag; } return status; } public void open() { open(true); } public void open(boolean animate) { if (animate) { //继续滑动 if(dragHelper.smoothSlideViewTo(vg_main, range, 0)) { ViewCompat.postInvalidateOnAnimation(this); } } else { vg_main.layout(range, 0, range * 2,height); dispatchDragEvent(range); } } public void close() { close(true); } public void close(boolean animate) { if (animate) { //继续滑动 if(dragHelper.smoothSlideViewTo(vg_main, 0, 0)) { ViewCompat.postInvalidateOnAnimation(this); } } else { vg_main.layout(0, 0, width,height); dispatchDragEvent(0); } } } ~~~ ## (七).最后总结 今天我们通过ViewDragHelper来讲解实现一个类似QQ5.x侧滑效果的组件的效果,另外该项目中也用到了沉浸式状态的效果。关于该效果的具体实现大家可以看我另一篇文章[(点击学习Android沉浸式状态栏)](http://blog.csdn.net/developer_jiangqq/article/details/49446855) 本次具体实例注释过的全部代码已经上传到Github项目中了。同时欢迎大家去Github站点进行clone或者下载浏览: [https://github.com/jiangqqlmj/ViewDragHelperTest](https://github.com/jiangqqlmj/ViewDragHelperTest) 同时欢迎大家star和fork整个开源快速开发框架项目~ 同时致谢: [https://github.com/BlueMor/DragLayout](https://github.com/BlueMor/DragLayout)
';

神器ViewDragHelper完全解析,妈妈再也不担心我自定义ViewGroup滑动View操作啦~(三十三)

最后更新于:2022-04-01 07:13:33

## (一).前言: 这几天正在更新录制实战项目,整体框架是采用仿照QQ5.X侧滑效果的。那么我们一般的做法就是自定义ViewGroup或者采用开源项目MenuDrawer或者Google提供的控件DrawerLayout等方式来实现。这些的控件的很多效果基本上都是采用实现onInterceptTouchEvent和onTouchEvent这两个方法进行实现,而且都是根据要实现的效果做自定义处理例如:多点触控处理,加速度方面检测以及控制等等。一般这样做作于普通开发人员来讲也是需要很强的程序与逻辑开发能力,幸好Android开发框架给我们提供了一个组件ViewDragHelper。那么今天我们来具体实现和分析一下ViewDragHelper 具体代码已经上传到下面的项目中,欢迎各位去star和fork一下。 FastDev4Android框架项目地址:[https://github.com/jiangqqlmj/FastDev4Android](https://github.com/jiangqqlmj/FastDev4Android) ## (二).ViewDragHelper基本介绍: 1.官方介绍如下: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569c8ec037d15.jpg)        对于自定义ViewGroup而言这边的ViewDragHelper是一个很不错的实用程序类。它给我们提供一系列的方法和相关状态,让我们可以进行拖拽移动或者重新定位ViewGroup中子视图View。ViewDragHelper是一个简化View的拖拽操作的帮助类,使用起来比较简单与方便,一般我们只需要实现几个方法和一个CallBack类就可以实现拖动的View。 2.在使用过程中我们一般会通过ViewDragHelper.Callback来进行连接ViewDragHelper和View。ViewDragHelper提供了一个静态方法让我们来创建实例,使用ViewDragHelper我们可以控制拖动的方向以及检测是否已经拖动到屏幕的边缘等相关操作。 3.ViewGragHelper.Callback中的相关方法说明: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569c8ec04bd7e.jpg) Callback中的方法如下,其他包括一个抽象方法tryCaptureView()和十二个一般方法组成 | tryCaptureView(View,int)|传递当前触摸的子View实例,如果当前的子View需要进行拖拽移动返回true| |--|--| | clampViewPositionHorizontal|决定拖拽的View在水平方向上面移动到的位置| | clampViewPositionVertical|决定拖拽的View在垂直方向上面移动到的位置| | getViewHorizontalDragRange|返回一个大于0的数,然后才会在水平方向移动| | getViewVerticalDragRange|返回一个大于0的数,然后才会在垂直方向移动| | onViewDragStateChanged| 拖拽状态发生变化回调| | onViewPositionChanged|当拖拽的View的位置发生变化的时候回调(特指capturedview)| | onViewCaptured| 捕获captureview的时候回调| | onViewReleased| 当拖拽的View手指释放的时候回调| | onEdgeTouched|当触摸屏幕边界的时候回调 | | onEdgeLock| 是否锁住边界| | onEdgeDrageStarted|在边缘滑动的时候可以设置滑动另一个子View跟着滑动| | getOrderedChildIndex| | 上面简单讲解了其中的一些方法说明,下面我们就需要使用这些方法以及ViewDragHelper本身提供的初始化方法以及设置方法来简要说明使用ViewDragHelper使用流程。 ## (三).ViewDragHelper流程实例:   下面我们开始具体来使用ViewDragHelper了。 1.获取ViewGragHelper实例:我们通过使用ViewDragHelper的一个静态方法来创建实例: ~~~ public static ViewDragHelper create(ViewGroup forParent, float sensitivity, Callback cb) { final ViewDragHelper helper =create(forParent, cb); helper.mTouchSlop = (int)(helper.mTouchSlop * (1 / sensitivity)); return helper; } ~~~ * 参数1.一个ViewGroup,也就是拖动子View的父控件(ViewGroup) * 参数2.灵敏度一般设置成1.0f,表示灵敏度最敏感 * 参数3.拖拽回调,用来处理拖动的位置等相关操作 具体使用如下: ~~~ mDragHelper =ViewDragHelper.create(this, 1.0f, new DragHelperCallback()); ~~~ 2.继承ViewGragHelper.Callback类实现一个抽象方法:tryCaptureView()然后在内部进行处理需要捕获的子View(用于拖拽操作和移动) ~~~ /** * 进行捕获拦截,那些View可以进行drag操作 * @param child * @param pointerId * @return 直接返回true,拦截所有的VIEW */ @Override public boolean tryCaptureView(Viewchild, int pointerId) { return true; } ~~~ 这样表示捕捉所有的子View,表示所有的子View都可以进行拖拽操作。 3.重写onInterceptTouchView和onTouchEvent方法来拦截事件以及让ViewDragHelper来进行处理拦截到得事件。因为ViewDragHelper的内部是根据触摸等相关事件来实现拖拽的。 ~~~ /** * 事件分发 * @param ev * @return */ @Override public boolean onInterceptTouchEvent(MotionEvent ev) { returnmDragHelper.shouldInterceptTouchEvent(ev); } @Override public boolean onTouchEvent(MotionEvent ev){ mDragHelper.processTouchEvent(ev); return true; } ~~~ 4.拖动行为处理,例如我们现在需要处理横向的拖拽的,那么我们需要实现clampViewPositionHorizontal方法,并且返回一个适当的数值表示横向拖拽的效果。 一般返回第二个参数即可,不过需要对边界值做一下判断这样子View不要拖出屏幕。 【注】要实现横向滑动这个方法必须要重写,因为ViewGragHelper内部该方法的时候直接返回了0,看下内部实现代码: ~~~ public int clampViewPositionHorizontal(Viewchild, int left, int dx) { return 0; } ~~~ 我们重写的代码如下: ~~~ /** * 水平滑动 控制left * @param child * @param left * @param dx * @return */ @Override public int clampViewPositionHorizontal(View child, int left, int dx) { Log.d("DragLayout","clampViewPositionHorizontal " + left + "," + dx); final int leftBound =getPaddingLeft(); final int rightBound = getWidth() -view_one.getWidth(); final int newLeft =Math.min(Math.max(left, leftBound), rightBound); return newLeft; } ~~~ 同样对于垂直方向拖拽的重写clampViewPositionVertical()基本方法也差不多如下: ~~~ /** * 垂直滑动,控制top * @param child * @param top * @param dy * @return */ @Override public int clampViewPositionVertical(View child, int top, int dy) { Log.d("DragLayout","clampViewPositionVertical " + top + "," + dy); final int topBound =getPaddingTop(); final int bottomBound = getHeight()- view_one.getHeight(); final int newTop =Math.min(Math.max(top, topBound), bottomBound); return newTop; } ~~~ 5.基本功能实现核心代码已经做完了,下面我们来实现一个布局文件:自定义组件ViewGragOne内部放入了两个TextView子View,到时候我们主要拖拽这两个子View即可。 ~~~ <?xmlversion="1.0" encoding="utf-8"?> <LinearLayoutxmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical"android:layout_width="match_parent" android:layout_height="match_parent"> <includelayout="@layout/common_top_bar_layout"/> <com.chinaztt.fda.test.ViewGragHelper.ViewGragOne android:layout_width="fill_parent" android:layout_height="fill_parent"> <TextView android:id="@+id/tv_one" android:layout_width="120dp" android:layout_height="120dp" android:layout_margin="10dp" android:text="TV_ONE" android:gravity="center" android:background="@color/color_1"/> <TextView android:id="@+id/tv_two" android:layout_width="120dp" android:layout_height="120dp" android:layout_marginTop="200dp" android:layout_marginLeft="50dp" android:text="TV_TWO" android:gravity="center" android:background="@color/color_3"/> </com.chinaztt.fda.test.ViewGragHelper.ViewGragOne> </LinearLayout> ~~~ 5.自定义组件ViewGragOnew继承自LinerLayout,然后在内部采用ViewDragHelper做相关操作,具体包括以上核心操作的代码如下: ~~~ public class ViewGragOne extends LinearLayout{ private View view_one,view_two; private ViewDragHelper mDragHelper; public ViewGragOne(Context context) { this(context, null); } public ViewGragOne(Context context,AttributeSet attrs) { this(context, attrs, 0); } public ViewGragOne(Context context,AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); mDragHelper =ViewDragHelper.create(this, 1.0f, new DragHelperCallback()); } class DragHelperCallback extends Callback { /** * 进行捕获拦截,那些View可以进行drag操作 * @param child * @param pointerId * @return 直接返回true,拦截所有的VIEW */ @Override public boolean tryCaptureView(Viewchild, int pointerId) { return true; } /** * 水平滑动 控制left * @param child * @param left * @param dx * @return */ @Override public int clampViewPositionHorizontal(View child, int left, int dx) { Log.d("DragLayout","clampViewPositionHorizontal " + left + "," + dx); final int leftBound =getPaddingLeft(); final int rightBound = getWidth() -view_one.getWidth(); final int newLeft =Math.min(Math.max(left, leftBound), rightBound); return newLeft; } /** * 垂直滑动,控制top * @param child * @param top * @param dy * @return */ @Override public int clampViewPositionVertical(View child, int top, int dy) { Log.d("DragLayout","clampViewPositionVertical " + top + "," + dy); final int topBound =getPaddingTop(); final int bottomBound = getHeight()- view_one.getHeight(); final int newTop =Math.min(Math.max(top, topBound), bottomBound); return newTop; } } /** * 事件分发 * @param ev * @return */ @Override public boolean onInterceptTouchEvent(MotionEvent ev) { returnmDragHelper.shouldInterceptTouchEvent(ev); } @Override public boolean onTouchEvent(MotionEvent ev){ mDragHelper.processTouchEvent(ev); return true; } @Override protected void onFinishInflate() { super.onFinishInflate(); view_one=getChildAt(0); view_two=getChildAt(1); } } ~~~ 6.运行效果如下: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569c8ec06d9c0.jpg) ## (四).ViewDragHelper进阶讲解:   1.tryCaptureView该方法可以进行选择性拦截可以拖拽的子View,那么我们这边选择只拦截第一个View,那么第二个View就无法进行拖拽啦,具体实现方法如下: ~~~ @Override public boolean tryCaptureView(Viewchild, int pointerId) { return child==view_one; } ~~~ 实现效果如下: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569c8ec0d047a.jpg) 2.滑动边缘相关处理方法setEdgeTrackingEnabled(),onEdgeTouch()以及onEdgeDragStart()。 我们可以给ViewDragHelper加入滑动的边缘设置,组件给我们提供了如下边缘控制值:EDGE_LEFT,EDGE_RIGHT,EDGE_TOP,EDGE_BOTTOM和EDGE_ALL,分别控制上下左右以及全部边缘控制。我们这边设置左边缘: mDragHelper.setEdgeTrackingEnabled(ViewDragHelper.EDGE_LEFT); 那么根据上面我们的方法介绍,我们可以重写onEdgeTouched()方法来拦截触摸到边缘的动作。 ~~~ @Override public void onEdgeTouched(intedgeFlags, int pointerId) { super.onEdgeTouched(edgeFlags,pointerId); Toast.makeText(getContext(),"edgeTouched", Toast.LENGTH_SHORT).show(); } ~~~ 如果要实现我们的手指在边缘进行滑动的时候,同时根据滑动的距离来滑动另外一个View,我们可以重写onEdgeDragStared()方法,在内部调用caturedChildView()方法实现即可。(这个效果我们就可以联想到侧滑界面效果,滑动时候可以打开主布局界面),具体实现代码如下: ~~~ /** * 在边界滑动的时候 同时滑动dragView2 * @param edgeFlags * @param pointerId */ @Override public void onEdgeDragStarted(intedgeFlags, int pointerId) { mDragHelper.captureChildView(view_two, pointerId); } ~~~ 运行效果如下: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569c8ec131435.jpg) 3.除了以上的方法之外,我们还有一个手指触摸释放之后回调的方法,onViewReleased()。这个好比侧滑组件中我们点击一个位置,然后界面自动打开或者关闭的效果。 ~~~ /** * 当手指松开的时候回调方法 * @param releasedChild 滑动手指松开的View * @param xvel * @param yvel */ @Override public void onViewReleased(View releasedChild, float xvel, float yvel) { super.onViewReleased(releasedChild,xvel, yvel); Log.d("zttjiangqq","onViewReleased"); } ~~~ 当我们拖拽一个子View,然后手指释放之后运行效果如下: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569c8ec1b7fe9.jpg) 4.ViewGragOne全部实例代码如下: ~~~ package com.chinaztt.fda.test.ViewGragHelper; import android.content.Context; import android.support.v4.view.MotionEventCompat; import android.support.v4.widget.ViewDragHelper; import android.support.v4.widget.ViewDragHelper.Callback; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.View; import android.widget.LinearLayout; import android.widget.Toast; import com.chinaztt.fda.utils.Log; /** * 当前类注释: * 项目名:FastDev4Android * 包名:com.chinaztt.fda.test.ViewGragHelper * 作者:江清清 on 15/11/24 20:29 * 邮箱:jiangqqlmj@163.com * QQ: 781931404 * 公司:江苏中天科技软件技术有限公司 */ public class ViewGragOne extends LinearLayout{ private View view_one,view_two; private ViewDragHelper mDragHelper; public ViewGragOne(Context context) { this(context, null); } public ViewGragOne(Context context,AttributeSet attrs) { this(context, attrs, 0); } public ViewGragOne(Context context,AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); mDragHelper =ViewDragHelper.create(this, 1.0f, new DragHelperCallback()); mDragHelper.setEdgeTrackingEnabled(ViewDragHelper.EDGE_LEFT); } class DragHelperCallback extends Callback { /** * 进行捕获拦截,那些View可以进行drag操作 * @param child * @param pointerId * @return 直接返回true,拦截所有的VIEW */ @Override public boolean tryCaptureView(Viewchild, int pointerId) { return true; } /** * 水平滑动 控制left * @param child * @param left * @param dx * @return */ @Override public int clampViewPositionHorizontal(View child, int left, int dx) { Log.d("DragLayout","clampViewPositionHorizontal " + left + "," + dx); final int leftBound =getPaddingLeft(); final int rightBound = getWidth() -view_one.getWidth(); final int newLeft =Math.min(Math.max(left, leftBound), rightBound); return newLeft; } /** * 垂直滑动,控制top * @param child * @param top * @param dy * @return */ @Override public int clampViewPositionVertical(View child, int top, int dy) { Log.d("DragLayout","clampViewPositionVertical " + top + "," + dy); final int topBound =getPaddingTop(); final int bottomBound = getHeight()- view_one.getHeight(); final int newTop =Math.min(Math.max(top, topBound), bottomBound); return newTop; } @Override public void onEdgeTouched(int edgeFlags, int pointerId) { super.onEdgeTouched(edgeFlags,pointerId); Toast.makeText(getContext(),"edgeTouched", Toast.LENGTH_SHORT).show(); } /** * 在边界滑动的时候 同时滑动dragView2 * @param edgeFlags * @param pointerId */ @Override public void onEdgeDragStarted(int edgeFlags, int pointerId) { mDragHelper.captureChildView(view_two, pointerId); } /** * 当手指松开的时候回调方法 * @paramreleasedChild 滑动手指松开的View * @param xvel * @param yvel */ @Override public void onViewReleased(View releasedChild, float xvel, float yvel) { super.onViewReleased(releasedChild,xvel, yvel); Log.d("zttjiangqq","onViewReleased"); } } /** * 事件分发 * @param ev * @return */ @Override public boolean onInterceptTouchEvent(MotionEvent ev) { return mDragHelper.shouldInterceptTouchEvent(ev); } @Override public boolean onTouchEvent(MotionEvent ev){ mDragHelper.processTouchEvent(ev); return true; } @Override protected void onFinishInflate() { super.onFinishInflate(); view_one=getChildAt(0); view_two=getChildAt(1); } } ~~~ ## (五).最后总结 今天我们对于ViewDragHelper的基本使用方法和相关流程做了详解,下一篇我们会通过ViewDragHelper来讲解实现一个类似QQ5.x侧滑效果的组件以及ViewDragHelper源代码解析。 本次具体实例注释过的全部代码已经上传到FastDev4Android项目中了。同时欢迎大家去Github站点进行clone或者下载浏览: [https://github.com/jiangqqlmj/FastDev4Android](https://github.com/jiangqqlmj/FastDev4Android) 同时欢迎大家star和fork整个开源快速开发框架项目~
';

CardView完全解析与RecyclerView结合使用(三十二)

最后更新于:2022-04-01 07:13:31

## (一).前言: 作为Android L开始,Google更新的除了RecyclerView之外的另一控件就是CardView,其中Google官方应用Google Now就采用了CardView控件,下面我们详细了解一下CardView和使用方法。 ## (二).基本介绍: CardView继承自FrameLayout,可以让我们使用类似卡片布局来显示一致性效果的内容。同时卡片还可以包含圆角和阴影效果。CardView是一个Layout,同时在里边布局其他View控件。如果我们需要创建带有一个阴影效果的卡片,那么可以使用card_view:cardElevation属性。 在API21(Android L)等级以上拥有属性elevation,意为CardView的Z轴阴影,只有L平台有效。只能通过xml中的elevation属性指定;另外我们还可以使用以下的属性来自定义CardView布局: 使用card_view:cardCornerRadius来设置布局的圆角。同样可以使用代码如下的代码设置圆角:CardView.setRadius。对于卡片的背景可以使用card_view:cardBackgroundColor设置。 CardView的其他属性以及作用如下: | card_view:cardElevation| 阴影的大小| |--|--| | card_view:cardMaxElevation| 阴影最大值| |card_view:cardBackgroundCollor|卡片的背景色| | card_view:cardCornerRadius|卡片的圆角大小| | card_view:contentPadding| 卡片内容与边距的间隔| | card_view:contentPaddingBottom| 卡片内容与底部的间隔| | card_view:contentPaddingTop|卡片内容与顶部的间隔| |card_view:contentPaddingLeft| 卡片内容与左边的间隔| | card_view:contentPaddingRight | 卡片内容与右边的间隔| | card_view:contentPaddingStart| | | card_view:contentPaddingEnd| | ## (三).CardView组件引入: 我们知道CardView组件是Android L开始引入,同时Google也做了兼容包在V7包里边了,具体我们来看一下项目存在的路径: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569c8ebf988d8.jpg)使用Android Studio IDE进行开发的话,我们只需要进行dependencies引入依赖即可: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569c8ebfb45b4.jpg) ~~~ dependencies { compile fileTree(dir: 'libs', include:['*.jar']) ……. compile'com.android.support:cardview-v7:23.1.1' } ~~~ ## (四).CardView基本使用: 下面我们来具体来使用一下,根据上面的讲解我们知道CardView也是一个Layout,那么里边我们也可以加入其他布局,请看实例Demo布局文件,比较简单一看就会了: ~~~ <?xmlversion="1.0" encoding="utf-8"?> <LinearLayoutxmlns:android="http://schemas.android.com/apk/res/android" xmlns:card_view="http://schemas.android.com/apk/res-auto" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent"> <includelayout="@layout/common_top_bar_layout"/> <android.support.v7.widget.CardView android:layout_marginTop="3dp" android:layout_marginLeft="3dp" android:layout_marginRight="3dp" android:id="@+id/card_view_one" android:layout_gravity="center" android:layout_width="match_parent" android:layout_height="100dp" card_view:cardCornerRadius="5dp"> <TextView android:id="@+id/info_text_one" android:text="CardView1测试" android:textSize="16sp" android:gravity="center" android:layout_width="match_parent" android:layout_height="match_parent" /> </android.support.v7.widget.CardView> <android.support.v7.widget.CardView android:layout_marginTop="3dp" android:layout_marginLeft="3dp" android:layout_marginRight="3dp" android:id="@+id/card_view_two" android:layout_gravity="center" android:layout_width="match_parent" android:layout_height="100dp" card_view:cardCornerRadius="5dp" card_view:cardBackgroundColor="#FFE4B5"> <TextView android:id="@+id/info_text_two" android:text="CardView2测试" android:textSize="16sp" android:gravity="center" android:layout_width="match_parent" android:layout_height="match_parent" /> </android.support.v7.widget.CardView> <android.support.v7.widget.CardView android:layout_marginTop="3dp" android:layout_marginLeft="3dp" android:layout_marginRight="3dp" android:id="@+id/card_view_three" android:layout_gravity="center" android:layout_width="match_parent" android:layout_height="100dp" card_view:cardCornerRadius="5dp" card_view:cardBackgroundColor="#CAE1FF"> <TextView android:id="@+id/info_text_three" android:text="CardView3测试" android:textSize="16sp" android:gravity="center" android:layout_width="match_parent" android:layout_height="match_parent" /> </android.support.v7.widget.CardView> <android.support.v7.widget.CardView android:layout_marginTop="3dp" android:layout_marginLeft="3dp" android:layout_marginRight="3dp" android:id="@+id/card_view_four" android:layout_gravity="center" android:layout_width="match_parent" android:layout_height="100dp" card_view:cardCornerRadius="5dp" card_view:cardBackgroundColor="#7CCD7C" card_view:cardElevation="5dp" card_view:cardMaxElevation="5dp"> <TextView android:id="@+id/info_text_four" android:text="CardView4测试" android:textSize="16sp" android:gravity="center" android:layout_width="match_parent" android:layout_height="match_parent" /> </android.support.v7.widget.CardView> </LinearLayout> ~~~ 这边定义了四个CardView,同时设置了圆角以及相关背景颜色,其他第四个CardView还设置了阴影效果,具体运行效果如下: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569c8ebfcded5.jpg) ## (五).CardView结合RecyclerView: 前面我们实现了RecyclerView实现列表功能了,现在我们把Item View采用CardView来实现,     1.首先我们来看下Item 布局文件: ~~~ <?xmlversion="1.0" encoding="utf-8"?> <LinearLayoutxmlns:android="http://schemas.android.com/apk/res/android" xmlns:card_view="http://schemas.android.com/apk/res-auto" android:orientation="vertical"android:layout_width="match_parent" android:layout_height="match_parent"> <android.support.v7.widget.CardView android:id="@+id/item_cardview" android:layout_gravity="center" android:layout_width="match_parent" android:layout_height="100dp" card_view:cardCornerRadius="5dp" card_view:cardBackgroundColor="#7CCD7C" card_view:cardElevation="3dp" card_view:cardMaxElevation="5dp"> <TextView android:id="@+id/item_tv" android:text="CardView测试" android:textSize="16sp" android:textColor="#FFFFFF" android:gravity="center" android:layout_width="match_parent" android:layout_height="match_parent" /> </android.support.v7.widget.CardView> </LinearLayout> ~~~ 这个布局比较简单,定义一个CardView,内部包含了一个TextView。 2.自定义的Adapter如下,主要加载布局然后绑定数据: ~~~ packagecom.chinaztt.fda.test.CardView; importandroid.content.Context; importandroid.support.v7.widget.CardView; importandroid.support.v7.widget.RecyclerView; importandroid.text.Layout; importandroid.view.LayoutInflater; importandroid.view.View; importandroid.view.ViewGroup; importandroid.widget.TextView; importcom.chinaztt.fda.entity.CardDataUtils; importcom.chinaztt.fda.entity.CardViewBean; importcom.chinaztt.fda.ui.R; importjava.util.List; /** * 当前类注释:CardView结合RecyclerView 自定义Adapter * 项目名:FastDev4Android * 包名:com.chinaztt.fda.test.CardView * 作者:江清清 on 15/11/23 19:41 * 邮箱:jiangqqlmj@163.com * QQ: 781931404 * 公司:江苏中天科技软件技术有限公司 */ public class CardViewAdapter extends RecyclerView.Adapter<CardViewAdapter.ItemCardViewHolder>{ private List<CardViewBean> beans; private LayoutInflater mInflater; private Context mContext; public CardViewAdapter(Context context){ this.mContext=context; beans=CardDataUtils.getCardViewDatas(); mInflater=LayoutInflater.from(context); } @Override public ItemCardViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { Viewview=mInflater.inflate(R.layout.item_cardview_layout,parent,false); return new ItemCardViewHolder(view); } @Override public void onBindViewHolder(ItemCardViewHolder holder, int position) { holder.item_cardview.setCardBackgroundColor(mContext.getResources().getColor(beans.get(position).getColor())); holder.item_tv.setText(beans.get(position).getTitle()); } @Override public int getItemCount() { return beans.size(); } public static class ItemCardViewHolder extends RecyclerView.ViewHolder{ private CardView item_cardview; private TextView item_tv; public ItemCardViewHolder(ViewitemView) { super(itemView); item_cardview=(CardView)itemView.findViewById(R.id.item_cardview); item_tv=(TextView)itemView.findViewById(R.id.item_tv); } } } ~~~ 3.数据提供我这边采用一个类,在内边进行构造的: ~~~ public class CardDataUtils { public static List<CardViewBean> getCardViewDatas(){ List<CardViewBean> beans=new ArrayList<CardViewBean>(); int[] colors=newint[]{R.color.color_0,R.color.color_1, R.color.color_2,R.color.color_3,R.color.color_4, R.color.color_5,R.color.color_6,R.color.color_7, R.color.color_8,R.color.color_9,R.color.color_10,}; for(int i=0;i<11;i++){ beans.add(new CardViewBean(colors[i],"CardView测试Item"+i)); } return beans; } } ~~~ 其中里面的11个颜色在,colors文件中定义了: ~~~ <!--cardView 实例中的颜色--> <color name="color_0">#FFF0F5</color> <color name="color_1">#FFE1FF</color> <color name="color_2">#E6E6FA</color> <color name="color_3">#C1FFC1</color> <color name="color_4">#B22222</color> <color name="color_5">#836FFF</color> <color name="color_6">#68228B</color> <color name="color_7">#5CACEE</color> <color name="color_8">#43CD80</color> <color name="color_9">#00EE00</color> <color name="color_10">#708090</color> ~~~ 4.主布局文件的代码就不贴了,和以前一样,就是一个RecyclerView控件定义,然后主Activity使用方法代码如下: ~~~ packagecom.chinaztt.fda.test.CardView; importandroid.os.Bundle; importandroid.support.v7.widget.LinearLayoutManager; importandroid.support.v7.widget.OrientationHelper; importandroid.support.v7.widget.RecyclerView; importandroid.view.View; importandroid.widget.LinearLayout; importandroid.widget.TextView; importcom.chinaztt.fda.ui.R; importcom.chinaztt.fda.ui.base.BaseActivity; /** * 当前类注释:CardView结合RecyclerView使用实例 * 项目名:FastDev4Android * 包名:com.chinaztt.fda.test.CardView * 作者:江清清 on 15/11/23 19:34 * 邮箱:jiangqqlmj@163.com * QQ: 781931404 * 公司:江苏中天科技软件技术有限公司 */ public class CardViewRecyclerActivity extends BaseActivity { private LinearLayout top_bar_linear_back; private TextView top_bar_title; private RecyclerView recycler_cardview; @Override protected void onCreate(BundlesavedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.card_view_recycler_layout); top_bar_linear_back=(LinearLayout)this.findViewById(R.id.top_bar_linear_back); top_bar_linear_back.setOnClickListener(new CustomOnClickListener()); top_bar_title=(TextView)this.findViewById(R.id.top_bar_title); top_bar_title.setText("CardView结合RecyclerView使用实例"); recycler_cardview=(RecyclerView)this.findViewById(R.id.recycler_cardview); LinearLayoutManager linearLayoutManager=new LinearLayoutManager(this); linearLayoutManager.setOrientation(OrientationHelper.VERTICAL); recycler_cardview.setLayoutManager(linearLayoutManager); recycler_cardview.setAdapter(newCardViewAdapter(this)); } class CustomOnClickListener implements View.OnClickListener{ @Override public void onClick(View v) { CardViewRecyclerActivity.this.finish(); } } } ~~~ 5.运行效果如下: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569c8ebfe8f4d.jpg) ## (六).最后总结 今天我们对于CardView做了基本讲解以及相关使用方法,同时也通过与RecyclerView的结合让我们巩固CardView的使用。CardView控件整体比较简单,相信大家还是比较容易理解,以后的项目中大家就可以使用CardView控件了(例如在新闻列表功能中等)。 本次具体实例注释过的全部代码已经上传到FastDev4Android项目中了。同时欢迎大家去Github站点进行clone或者下载浏览: [https://github.com/jiangqqlmj/FastDev4Android](https://github.com/jiangqqlmj/FastDev4Android) 同时欢迎大家star和fork整个开源快速开发框架项目~ 本人录制AA(Android Annotations)注入框架的视频教程已经上线了,欢迎大家前往观看。[http://www.cniao5.com/course/10074](http://www.cniao5.com/course/10074) [![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569c8ebc8324c.jpg)](http://www.cniao5.com/course/10074)
';

RecyclerView完全解析之下拉刷新与上拉加载SwipeRefreshLayout(三十一)

最后更新于:2022-04-01 07:13:29

## (一).前言: 话说RecyclerView已经面市很久,也在很多应用中得到广泛的使用,在整个开发者圈子里面也拥有很不错的口碑,那说明RecyclerView拥有比ListView,GridView之类控件有很多的优点,例如:数据绑定,Item View创建,View的回收以及重用等机制。前三三篇文章已经贡呢更新了以下三个部分: 1. RecyclerView控件的基本使用,包括基础,进阶,高级部分,动画之类[(点击进入)](http://blog.csdn.net/developer_jiangqq/article/details/49927631) 2. RecyclerView控件的实战实例[(点击进入)](http://blog.csdn.net/developer_jiangqq/article/details/49946589) 3. RecyclerView控件集合AA(Android Annotations)注入框架实例[(点击进入)](http://blog.csdn.net/developer_jiangqq/article/details/49967587) 本来这个专题不打算更新,不过前两天看到各位童鞋还是挺积极的评论到,希望可以更新RecyclerView加入下拉刷新和上拉加载更多的功能。正好昨天周末,所以我这边也就实现了这样的功能,今天更新一下。具体代码已经上传到下面的项目中,欢迎各位去star和fork一下。 FastDev4Android框架项目地址:[https://github.com/jiangqqlmj/FastDev4Android](https://github.com/jiangqqlmj/FastDev4Android) RecyclerView实现的列表,默认情况下面是不带下拉刷新和上拉记载更多效果的,但是我在我们的实际项目当中,为了提高用户体验,这种效果一般都需要实现,在我以前的博客中已经重写了ListView[(Android 列表下拉刷新组件PullToRefreshListView使用)](http://blog.csdn.net/developer_jiangqq/article/details/49383417)实现上拉刷新和上拉加载更多效果。现在我们已经学会了ListView,GridView的替代品RecyclerView的基本使用方法,那么必不可少的也需要实现上拉刷新和上拉加载更多的效果了。今天我们会通过两种方式来实现,具体会采用Android的另一控件SwipeRefreshLayout。 ## (二).SwipeRefreshLayout介绍: SwipeRefrshLayout是Google官方更新的一个Widget,可以实现下拉刷新的效果。该控件集成自ViewGroup在support-v4兼容包下,不过我们需要升级supportlibrary的版本到19.1以上。基本使用的方法如下: * setOnRefreshListener(OnRefreshListener):添加下拉刷新监听器 * setRefreshing(boolean):显示或者隐藏刷新进度条 * isRefreshing():检查是否处于刷新状态 * setColorSchemeResources():设置进度条的颜色主题,最多设置四种,以前的setColorScheme()方法已经弃用了。 具体使用效果下面我们会看到。 ## (三).RecyclerView+SwpieRefreshLayout实现下拉刷新效果: 1.SwipeRefreshLayout本身自带下拉刷新的效果,那么我们可以选择在RecyclerView布局外部嵌套一层SwipeRefreshLayout布局即可,具体布局文件如下: ~~~ <?xmlversion="1.0" encoding="utf-8"?> <LinearLayoutxmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical"android:layout_width="match_parent" android:layout_height="match_parent"> <includelayout="@layout/common_top_bar_layout"/> <android.support.v4.widget.SwipeRefreshLayout android:id="@+id/demo_swiperefreshlayout" android:layout_width="fill_parent" android:layout_height="fill_parent" android:scrollbars="vertical" > <android.support.v7.widget.RecyclerView android:id="@+id/demo_recycler" android:layout_width="fill_parent" android:layout_height="fill_parent" ></android.support.v7.widget.RecyclerView> </android.support.v4.widget.SwipeRefreshLayout> </LinearLayout> ~~~ 2.接着在Activity中获取SwipeRefreshLayout控件并且设置OnRefreshListener监听器,同时实现里边的onRefresh()方法,在该方法中进行网络请求最新数据,然后刷新RecyclerView列表同时设置SwipeRefreshLayout的进度Bar的隐藏或者显示效果。具体代码如下: ~~~ demo_swiperefreshlayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() { @Override public void onRefresh() { Log.d("zttjiangqq","invoke onRefresh..."); new Handler().postDelayed(newRunnable() { @Override public void run() { List<String> newDatas = new ArrayList<String>(); for (int i = 0; i <5; i++) { int index = i + 1; newDatas.add("new item" + index); } adapter.addItem(newDatas); demo_swiperefreshlayout.setRefreshing(false); Toast.makeText(RecyclerRefreshActivity.this, "更新了五条数据...", Toast.LENGTH_SHORT).show(); } }, 5000); } }); ~~~ 3.除此之外我们也来看一下Adapter和Activity中的其他代码,也方便各位童鞋查看。 ~~~ RecyclerRefreshActivity.java public class RecyclerRefreshActivity extends BaseActivity { private LinearLayout top_bar_linear_back; private TextView top_bar_title; private SwipeRefreshLayout demo_swiperefreshlayout; private RecyclerView demo_recycler; private RefreshRecyclerAdapter adapter; private LinearLayoutManager linearLayoutManager; private int lastVisibleItem; @Override protected void onCreate(BundlesavedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.recycler_refresh_layout); top_bar_linear_back=(LinearLayout)this.findViewById(R.id.top_bar_linear_back); top_bar_linear_back.setOnClickListener(new CustomOnClickListener()); top_bar_title=(TextView)this.findViewById(R.id.top_bar_title); top_bar_title.setText("RecyclerView下拉刷新,下拉加载更多..."); demo_swiperefreshlayout=(SwipeRefreshLayout)this.findViewById(R.id.demo_swiperefreshlayout); demo_recycler=(RecyclerView)this.findViewById(R.id.demo_recycler); //设置刷新时动画的颜色,可以设置4个 demo_swiperefreshlayout.setProgressBackgroundColorSchemeResource(android.R.color.white); demo_swiperefreshlayout.setColorSchemeResources(android.R.color.holo_blue_light, android.R.color.holo_red_light,android.R.color.holo_orange_light, android.R.color.holo_green_light); demo_swiperefreshlayout.setProgressViewOffset(false, 0, (int) TypedValue .applyDimension(TypedValue.COMPLEX_UNIT_DIP, 24, getResources() .getDisplayMetrics())); linearLayoutManager=new LinearLayoutManager(this); linearLayoutManager.setOrientation(OrientationHelper.VERTICAL); demo_recycler.setLayoutManager(linearLayoutManager); //添加分隔线 demo_recycler.addItemDecoration(new AdvanceDecoration(this, OrientationHelper.VERTICAL)); demo_recycler.setAdapter(adapter = new RefreshRecyclerAdapter(this)); demo_swiperefreshlayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() { @Override public void onRefresh() { Log.d("zttjiangqq","invoke onRefresh..."); new Handler().postDelayed(newRunnable() { @Override public void run() { List<String> newDatas = new ArrayList<String>(); for (int i = 0; i <5; i++) { int index = i + 1; newDatas.add("new item" + index); } adapter.addItem(newDatas); demo_swiperefreshlayout.setRefreshing(false); Toast.makeText(RecyclerRefreshActivity.this, "更新了五条数据...", Toast.LENGTH_SHORT).show(); } }, 5000); } }); class CustomOnClickListenerimplements View.OnClickListener{ @Override public void onClick(View v) { RecyclerRefreshActivity.this.finish(); } } } ~~~ ~~~ RefreshRecyclerAdapter.java public class RefreshRecyclerAdapter extends RecyclerView.Adapter<RefreshRecyclerAdapter.ViewHolder>{ private LayoutInflater mInflater; private List<String> mTitles=null; public RefreshRecyclerAdapter(Context context){ this.mInflater=LayoutInflater.from(context); this.mTitles=new ArrayList<String>(); for (int i=0;i<20;i++){ int index=i+1; mTitles.add("item"+index); } } /** * item显示类型 * @param parent * @param viewType * @return */ @Override public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { final Viewview=mInflater.inflate(R.layout.item_recycler_layout,parent,false); //这边可以做一些属性设置,甚至事件监听绑定 //view.setBackgroundColor(Color.RED); ViewHolder viewHolder=new ViewHolder(view); return viewHolder; } /** * 数据的绑定显示 * @param holder * @param position */ @Override public void onBindViewHolder(ViewHolder holder, int position) { holder.item_tv.setText(mTitles.get(position)); holder.itemView.setTag(position); } @Override public int getItemCount() { return mTitles.size(); } //自定义的ViewHolder,持有每个Item的的所有界面元素 public static class ViewHolder extends RecyclerView.ViewHolder { public TextView item_tv; public ViewHolder(View view){ super(view); item_tv = (TextView)view.findViewById(R.id.item_tv); } } //添加数据 public void addItem(List<String> newDatas) { //mTitles.add(position, data); //notifyItemInserted(position); newDatas.addAll(mTitles); mTitles.removeAll(mTitles); mTitles.addAll(newDatas); notifyDataSetChanged(); } public void addMoreItem(List<String> newDatas) { mTitles.addAll(newDatas); notifyDataSetChanged(); } } ~~~ 以上重要地方的注释已经加上去了。 5.运行效果如下: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569c8ebe9c9c4.jpg) ## (四).RecyclerView设置滚动事件加入上拉加载更多功能 下面我们再来看RecyclerView和相关类的一些特性, LayoutManger给我们提供了以下几个方法来让开发者方便的获取到屏幕上面的顶部item和顶部item相关的信息: * findFirstVisibleItemPosition() * findFirstCompletlyVisibleItemPosition() * findLastVisibleItemPosition() * findLastCompletlyVisibleItemPosition() 同时通过Recycler.Adapter的getItemCount()方法可以轻松获取到RecyclerView列表中Item View的个数。 那么下面我们通过监听滑动(滚动)事件,然后在里边判断是否已经滑动到最底部来加载更多的数据,使用方法如下: ~~~ //RecyclerView滑动监听 demo_recycler.setOnScrollListener(new RecyclerView.OnScrollListener() { @Override public void onScrollStateChanged(RecyclerView recyclerView, int newState) { super.onScrollStateChanged(recyclerView, newState); if (newState ==RecyclerView.SCROLL_STATE_IDLE && lastVisibleItem + 1 ==adapter.getItemCount()) { new Handler().postDelayed(new Runnable() { @Override public void run() { List<String> newDatas = new ArrayList<String>(); for (int i = 0; i< 5; i++) { int index = i +1; newDatas.add("more item" + index); } adapter.addMoreItem(newDatas); } },1000); } } @Override public void onScrolled(RecyclerView recyclerView, int dx, int dy) { super.onScrolled(recyclerView,dx, dy); lastVisibleItem =linearLayoutManager.findLastVisibleItemPosition(); } }); ~~~ 运行效果如下: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569c8ebed0d25.jpg) ## (五).升级RecyclerView加入FootView实现上拉加载 上面我们虽然已经实现了上拉加载更多的效果,但是还比较丑陋,最起码要让用户知道确实在上拉加载的过程吧,例如加载一个底部的进度布局。这样一想,那么我们就按照ListView方式addFootView()呗,不过很可惜的是RecyclerView没有给我们提供addFootView()方法,那该怎么样办呢?我们来看RecyclerView.Apapter类: 我们要实现一个自定义Adapter一定需要实现onCreateViewHolder(ViewGroup paren,int viewType)方法,注意看方法中的第二个参数viewType,是不是想到布局类型了,也就是说该也支持多套布局显示的,那么查看基类中的所有方法如下: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569c8ebf1c843.jpg) 上面有一个方法getItemType(),这个就和ListView的Adapter的实现差不多了,那么我们这边可以使用多套布局给RecyclerView加入一个FootView布局即可。RefreshFootAdapter.java具体实现流程如下: 1.加入布局状态标志-用来判断此时加载是普通Item还是foot view: private static final int TYPE_ITEM =0;  //普通Item View private static final intTYPE_FOOTER = 1;  //顶部FootView 2.重写getItemCount()方法,返回的Item数量在数据的基础上面+1,增加一项FootView布局项 ~~~ public intgetItemCount() { return mTitles.size()+1; } ~~~ 3.重写getItemViewType方法来判断返回加载的布局的类型 ~~~ public int getItemViewType(int position) { // 最后一个item设置为footerView if (position + 1 == getItemCount()) { return TYPE_FOOTER; } else { return TYPE_ITEM; } } ~~~ 4.接着onCreateViewHolder(ViewGroup parent,int viewType)加载布局的时候根据viewType的类型来选择指定的布局创建,返回即可: ~~~ public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { //进行判断显示类型,来创建返回不同的View if(viewType==TYPE_ITEM){ Viewview=mInflater.inflate(R.layout.item_recycler_layout,parent,false); //这边可以做一些属性设置,甚至事件监听绑定 //view.setBackgroundColor(Color.RED); ItemViewHolder itemViewHolder=new ItemViewHolder(view); return itemViewHolder; }else if(viewType==TYPE_FOOTER){ Viewfoot_view=mInflater.inflate(R.layout.recycler_load_more_layout,parent,false); //这边可以做一些属性设置,甚至事件监听绑定 //view.setBackgroundColor(Color.RED); FootViewHolder footViewHolder=new FootViewHolder(foot_view); return footViewHolder; } return null; } ~~~ 5.最后进行判断数据的时候(onBindViewHolder),判断holder的类型来进行判定数据即可. ~~~ public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { if(holder instanceof ItemViewHolder) { ((ItemViewHolder)holder).item_tv.setText(mTitles.get(position)); holder.itemView.setTag(position); }else if(holder instanceof FootViewHolder){ FootViewHolderfootViewHolder=(FootViewHolder)holder; switch (load_more_status){ case PULLUP_LOAD_MORE: footViewHolder.foot_view_item_tv.setText("上拉加载更多..."); break; case LOADING_MORE: footViewHolder.foot_view_item_tv.setText("正在加载更多数据..."); break; } } } ~~~ 6.整个RefreshFootAdapter完整代码如下: ~~~ packagecom.chinaztt.fda.adapter; public class RefreshFootAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>{ //上拉加载更多 public static final int PULLUP_LOAD_MORE=0; //正在加载中 public static final int LOADING_MORE=1; //上拉加载更多状态-默认为0 private int load_more_status=0; private LayoutInflater mInflater; private List<String> mTitles=null; private static final intTYPE_ITEM = 0; //普通Item View private static final intTYPE_FOOTER = 1; //顶部FootView public RefreshFootAdapter(Context context){ this.mInflater=LayoutInflater.from(context); this.mTitles=new ArrayList<String>(); for (int i=0;i<20;i++){ int index=i+1; mTitles.add("item"+index); } } /** * item显示类型 * @param parent * @param viewType * @return */ public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { //进行判断显示类型,来创建返回不同的View if(viewType==TYPE_ITEM){ Viewview=mInflater.inflate(R.layout.item_recycler_layout,parent,false); //这边可以做一些属性设置,甚至事件监听绑定 //view.setBackgroundColor(Color.RED); ItemViewHolder itemViewHolder=new ItemViewHolder(view); return itemViewHolder; }else if(viewType==TYPE_FOOTER){ Viewfoot_view=mInflater.inflate(R.layout.recycler_load_more_layout,parent,false); //这边可以做一些属性设置,甚至事件监听绑定 //view.setBackgroundColor(Color.RED); FootViewHolder footViewHolder=new FootViewHolder(foot_view); return footViewHolder; } return null; } /** * 数据的绑定显示 * @param holder * @param position */ public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { if(holder instanceof ItemViewHolder) { ((ItemViewHolder)holder).item_tv.setText(mTitles.get(position)); holder.itemView.setTag(position); }else if(holder instanceof FootViewHolder){ FootViewHolder footViewHolder=(FootViewHolder)holder; switch (load_more_status){ case PULLUP_LOAD_MORE: footViewHolder.foot_view_item_tv.setText("上拉加载更多..."); break; case LOADING_MORE: footViewHolder.foot_view_item_tv.setText("正在加载更多数据..."); break; } } } /** * 进行判断是普通Item视图还是FootView视图 * @param position * @return */ @Override public int getItemViewType(int position) { // 最后一个item设置为footerView if (position + 1 == getItemCount()) { return TYPE_FOOTER; } else { return TYPE_ITEM; } } @Override public int getItemCount() { return mTitles.size()+1; } //自定义的ViewHolder,持有每个Item的的所有界面元素 public static class ItemViewHolder extends RecyclerView.ViewHolder { public TextView item_tv; public ItemViewHolder(View view){ super(view); item_tv = (TextView)view.findViewById(R.id.item_tv); } } /** * 底部FootView布局 */ public static class FootViewHolder extends RecyclerView.ViewHolder{ private TextView foot_view_item_tv; public FootViewHolder(View view) { super(view); foot_view_item_tv=(TextView)view.findViewById(R.id.foot_view_item_tv); } } //添加数据 public void addItem(List<String> newDatas) { //mTitles.add(position, data); //notifyItemInserted(position); newDatas.addAll(mTitles); mTitles.removeAll(mTitles); mTitles.addAll(newDatas); notifyDataSetChanged(); } public void addMoreItem(List<String> newDatas) { mTitles.addAll(newDatas); notifyDataSetChanged(); } /** * //上拉加载更多 * PULLUP_LOAD_MORE=0; * //正在加载中 * LOADING_MORE=1; * //加载完成已经没有更多数据了 * NO_MORE_DATA=2; * @param status */ public void changeMoreStatus(int status){ load_more_status=status; notifyDataSetChanged(); } } ~~~ 同时该Adaper中我还定义一个changeMoreStatus()方法和两个字符串常量可以来进行修改FootView中字符串提醒文本的。 7.整体Activity中还是设置监听RecyclerView的滚动事件.代码和第一个例子差不多,不过RecyclerView需要设置这边的Adapter了,demo_recycler.setAdapter(adapter= new RefreshFootAdapter(this)); ~~~ demo_recycler.setOnScrollListener(new RecyclerView.OnScrollListener() { @Override public void onScrollStateChanged(RecyclerView recyclerView, int newState) { super.onScrollStateChanged(recyclerView, newState); if (newState ==RecyclerView.SCROLL_STATE_IDLE && lastVisibleItem + 1 ==adapter.getItemCount()) { adapter.changeMoreStatus(RefreshFootAdapter.LOADING_MORE); newHandler().postDelayed(new Runnable() { @Override public void run() { List<String> newDatas = new ArrayList<String>(); for (int i = 0; i< 5; i++) { int index = i +1; newDatas.add("more item" + index); } adapter.addMoreItem(newDatas); adapter.changeMoreStatus(RefreshFootAdapter.PULLUP_LOAD_MORE); } }, 2500); } } @Override public void onScrolled(RecyclerView recyclerView, int dx, int dy) { super.onScrolled(recyclerView,dx, dy); lastVisibleItem =linearLayoutManager.findLastVisibleItemPosition(); } }); ~~~ 查看代码之后,大家肯定也发现在onScrollStateChanged()方法中,Adapter.addMoreItem()和Adapter.changeMoreStatus()方法内部都调用了notifyDataSetChanged()方法,也就是说这边刷新了两次,针对这个问题,大家可以把第二个方法放入到addMoreItem()内部去,调用一次刷新操作接口。同时也可以自己按照实际需求进行优化。 8.运行效果如下: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569c8ebf376ab.jpg) ## (六).最后总结 今天我们通过SwipeRefreshLayout和RecyclerView结合以及改造Adapter方式实现了RecyclerView的下拉刷新和上拉加载更多的效果。 本次具体实例注释过的全部代码已经上传到FastDev4Android项目中了。同时欢迎大家去Github站点进行clone或者下载浏览: [https://github.com/jiangqqlmj/FastDev4Android](https://github.com/jiangqqlmj/FastDev4Android) 同时欢迎大家star和fork整个开源快速开发框架项目~ 本人录制AA(Android Annotations)注入框架的视频教程已经上线了,欢迎大家前往观看。[http://www.cniao5.com/course/10074](http://www.cniao5.com/course/10074)
';

RecyclerView完全解析之结合AA(Android Annotations)注入框架实例(三十)

最后更新于:2022-04-01 07:13:26

## (一).前言: 话说RecyclerView已经面市很久,也在很多应用中得到广泛的使用,在整个开发者圈子里面也拥有很不错的口碑,那说明RecyclerView拥有比ListView,GridView之类控件有很多的优点,例如:数据绑定,Item View创建,View的回收以及重用等机制。本系列文章会包括到以下三个部分: 1. RecyclerView控件的基本使用,包括基础,进阶,高级部分,动画之类[(点击进入)](http://blog.csdn.net/developer_jiangqq/article/details/49927631) 2. RecyclerView控件的实战实例[(点击进入)](http://blog.csdn.net/developer_jiangqq/article/details/49946589) 3. RecyclerView控件集合AA(Android Annotations)注入框架实例 今天使我们本系列文章的第三讲主要使用RecyclerView结合AA(Android Annotations)注入框架实例。本次讲解所有用的Demo例子已经全部更新到下面的项目中了,欢迎大家star和fork。 [注]关于AA(Android Annotations)注入框架的使用方法,我在CSDN上面已经更新一个专题[(点击进入)](http://blog.csdn.net/column/details/aadeveloper.html),大家有兴趣可以去了解学习一下。 FastDev4Android框架项目地址:[https://github.com/jiangqqlmj/FastDev4Android](https://github.com/jiangqqlmj/FastDev4Android) ## (二).基本实现 这边演示的也是比较简单的效果,就是使用RecyclerView实现垂直滑动列表的效果。那么 对于每一项Item的布局如下: 1.Item布局文件:item_user_item.xml ~~~ <?xmlversion="1.0" encoding="utf-8"?> <RelativeLayoutxmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="49dp"> <TextView android:id="@+id/tv_first" android:text="fist name" android:textSize="16sp" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerVertical="true" android:layout_marginLeft="8dp"/> <TextView android:id="@+id/tv_last" android:text="last name" android:textSize="16sp" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerVertical="true" android:layout_alignParentRight="true" android:layout_marginRight="8dp"/> </RelativeLayout> ~~~ 该上面布局上面有两个文本框,然后创建一个实体类,里边两个属性: 2.TestUserBean.java ~~~ public class TestUserBean { private String firstName; private String LastName; public TestUserBean() { } public String getFirstName() { return firstName; } public void setFirstName(String firstName){ this.firstName = firstName; } public String getLastName() { return LastName; } public void setLastName(String lastName) { LastName = lastName; } @Override public String toString() { return "UserModel{" + "firstName='" +firstName + '\'' + ", LastName='" +LastName + '\'' + '}'; } } ~~~ 3.然后创建一个继承LinearLayout的控件来注入布局,同时加入绑定数据的方法,AAUserItemView.java,该类需要通过@EViewGroup来进行注入: ~~~ <span style="color:#ff0000;">@EViewGroup(R.layout.item_user_item)</span> public class AAUserItemView extends LinearLayout { @ViewById TextView tv_first; @ViewById TextView tv_last; public AAUserItemView(Context context) { super(context); } public void bind(TestUserBean userBean) { tv_first.setText(userBean.getFirstName()); tv_last.setText(userBean.getLastName()); } } ~~~ 4.因为这边是使用的RecyclerView而不是简单的ListView,那么这边需要特殊进行处理。RecyclerView.Adapter创建的是ViewHolder而不是View,所以这边就不能简单的注入ViewHolder类了。我们这边创建一个泛型类把所有的View包装成ViewHolder。 ~~~ /** * 当前类注释:创建一个泛型类来把所有类型的View包装成ViewHonlder * 项目名:FastDev4Android * 包名:com.chinaztt.fda.test.RecyclerViewAA * 作者:江清清 on 15/11/21 09:24 * 邮箱:jiangqqlmj@163.com * QQ: 781931404 * 公司:江苏中天科技软件技术有限公司 */ public class ViewWrapper<V extends View> extends RecyclerView.ViewHolder { public V view; public ViewWrapper(V itemView) { super(itemView); view = itemView; } public V getView() { return view; } } ~~~ 5.紧接着我们为所有的RecyclerView适配器创建一个基类(抽象类),继承RecyclerView.Adapter>。同时里边提供一个onCreateItemView(ViewGroup parent,intviewType)的抽象类,让调用者自己实现来创建View。然后里边提供一个数据集合,来存储绑定的数据。 ~~~ import android.support.v7.widget.RecyclerView; import android.view.View; import android.view.ViewGroup; import java.util.ArrayList; import java.util.List; public abstract class RecyclerViewAdapterBase<T,Vextends View>extends RecyclerView.Adapter<ViewWrapper<V>>{ /** * 存储需要绑定的数据 */ protected List<T> items = newArrayList<T>(); public List<T> getItems() { return items; } public void setItems(List<T> items) { this.items = items; } @Override public int getItemCount() { return items.size(); } /** * 进行创建视图承载类 * @param parent * @param viewType * @return */ @Override public final ViewWrapper<V>onCreateViewHolder(ViewGroup parent, int viewType) { return new ViewWrapper<V>(onCreateItemView(parent, viewType)); } /** * 创建视图Item,交给具体实现类完成 * @param parent * @param viewType * @return */ protected abstract VonCreateItemView(ViewGroup parent, int viewType); } ~~~ 6.下面我们来实现具体的Adapter,创建AAUserAdapter,并且这个类使用@EBean进行注解,继承刚刚的Adapter基类,RecyclerViewAdapterBase。主要实现以下两个方法: * protected AAUserItemView onCreateItemView(ViewGroup parent,int viewType) * Public void onBindViewHolder(ViewWrapper holder,int position) 分别进行创建View,最后调用onBindViewHolder()方法来进行绑定数据。 ~~~ package com.chinaztt.fda.test.RecyclerViewAA; import android.content.Context; import android.view.ViewGroup; import org.androidannotations.annotations.EBean; import org.androidannotations.annotations.RootContext; /** * 当前类注释: * 项目名:FastDev4Android * 包名:com.chinaztt.fda.test.RecyclerViewAA * 作者:江清清 on 15/11/21 09:35 * 邮箱:jiangqqlmj@163.com * QQ: 781931404 * 公司:江苏中天科技软件技术有限公司 */ <span style="color:#ff0000;">@EBean</span> public class AAUserAdapter extends RecyclerViewAdapterBase<TestUserBean,AAUserItemView> { @RootContext Context context; /** * 创建Item视图View * @param parent * @param viewType * @return */ @Override protected AAUserItemView onCreateItemView(ViewGroup parent, int viewType) { <span style="color:#ff0000;">return AAUserItemView_.build(context);</span> } /** * 进行绑定数据View * @param holder * @param position */ @Override public void onBindViewHolder(ViewWrapper<AAUserItemView> holder, int position) { AAUserItemView view =holder.getView(); TestUserBean userBean =items.get(position); view.bind(userBean); } } ~~~ 7.接下来需要进行使用RecyclerView和Adapter了,不过在使用之前首先需要准备数据,这边我们准备了一个接口类AAUserFinder.java ~~~ public interface AAUserFinder { List<TestUserBean> findAll(); } ~~~ 然后就是AAUserFinder的实现类ImMemoryUserFinder.这边实现接口中的findAll()方法,并且该类使用@EBean标签注解。 ~~~ packag ecom.chinaztt.fda.test.RecyclerViewAA; import org.androidannotations.annotations.EBean; import java.util.ArrayList; import java.util.List; /** * 当前类注释: * 项目名:FastDev4Android * 包名:com.chinaztt.fda.test.RecyclerViewAA * 作者:江清清 on 15/11/21 09:36 * 邮箱:jiangqqlmj@163.com * QQ: 781931404 * 公司:江苏中天科技软件技术有限公司 */ @EBean public class ImMemoryUserFinder implements AAUserFinder{ @Override public List<TestUserBean> findAll() { List<TestUserBean> userModels=new ArrayList<TestUserBean>(); for(int i=1;i<=45;i++){ TestUserBean model=new TestUserBean(); model.setFirstName("First 张三:"+i); model.setLastName("Last 李四:" + i); userModels.add(model); } return userModels; } } ~~~ 8.最后就是实例化RecyclerView以及使用AAUserAdapter了,该Activity采用@EActivity进行注解,通过@ViewById来注入初始化控件,通过@Bean来注入初始化AAUserAdapter以及AAUserFinder实例化(不过根据多态性,这边实例化的对象为ImMemoryUserFinder)。然后通过@AfterViews注解的方法发生调用,来创建布局管理器以及给Adapter提供数据,绑定数据即可。所有代码如下: ~~~ package com.chinaztt.fda.test; import android.os.Bundle; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.OrientationHelper; import android.support.v7.widget.RecyclerView; import android.view.View; import android.widget.LinearLayout; import android.widget.TextView; import com.chinaztt.fda.test.RecyclerViewAA.AAUserAdapter; import com.chinaztt.fda.test.RecyclerViewAA.AAUserFinder; import com.chinaztt.fda.test.RecyclerViewAA.ImMemoryUserFinder; import com.chinaztt.fda.ui.R; import com.chinaztt.fda.ui.base.BaseActivity; import org.androidannotations.annotations.AfterInject; import org.androidannotations.annotations.AfterViews; import org.androidannotations.annotations.Bean; import org.androidannotations.annotations.Click; import org.androidannotations.annotations.EActivity; import org.androidannotations.annotations.ViewById; /** * 当前类注释:RecyclerView集合AA(Android Annotations)注入框架实现实例 * 项目名:FastDev4Android * 包名:com.chinaztt.fda.test * 作者:江清清 on 15/11/20 14:41 * 邮箱:jiangqqlmj@163.com * QQ: 781931404 * 公司:江苏中天科技软件技术有限公司 */ <span style="color:#ff0000;">@EActivity(R.layout.recycler_aa_layout)</span> public class RecyclerViewAAActivity extends BaseActivity{ <span style="color:#ff0000;">@ViewById LinearLayout top_bar_linear_back; @ViewById TextView top_bar_title; @ViewById RecyclerView aa_recyclerview; @Bean AAUserAdapter adapter; @Bean(ImMemoryUserFinder.class) AAUserFinder userFinder;</span> @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); } <span style="color:#ff0000;">@AfterViews</span> public void initViews(){ top_bar_title.setText("RecyclerView集合AA注入框架实例"); //进行设置RecyerView ,并且绑定数据 LinearLayoutManager linearLayoutManager=new LinearLayoutManager(this); linearLayoutManager.setOrientation(OrientationHelper.VERTICAL); aa_recyclerview.setLayoutManager(linearLayoutManager); adapter.setItems(userFinder.findAll()); aa_recyclerview.setAdapter(adapter); } @Click(R.id.top_bar_linear_back) public void clickButton(View view){ this.finish(); } } ~~~ 9.运行效果如下: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569c8ebe3b149.jpg) 10.上面的类有很多AA(Android Annotations)注入框架标签的使用,如果各位童鞋对于AA框架不是太了解的话的,看上面的代码内容,会有点晕的~不过我已经对于AA注入学习的专题已经更新了[[AA(AndroidAnnotations)注入框架详解专题,点击进入...]](http://blog.csdn.net/column/details/aadeveloper.html) ## (三).最后总结 今天我们通过实例来演示了RecyclerView和AA注入框架的结合使用方法。 本次具体实例注释过的全部代码已经上传到FastDev4Android项目中了。同时欢迎大家去Github站点进行clone或者下载浏览: [https://github.com/jiangqqlmj/FastDev4Android](https://github.com/jiangqqlmj/FastDev4Android) 同时欢迎大家star和fork整个开源快速开发框架项目~后面应大家的要求,会更新RecyclerView下拉刷新和上拉更多效果以及RecyclerView打造的Gallery 3D版本的功能文章,敬请期待~
';

RecyclerView完全解析之打造新版类Gallery效果(二十九)

最后更新于:2022-04-01 07:13:24

## (一).前言: 话说RecyclerView已经面市很久,也在很多应用中得到广泛的使用,在整个开发者圈子里面也拥有很不错的口碑,那说明RecyclerView拥有比ListView,GridView之类控件有很多的优点,例如:数据绑定,Item View创建,View的回收以及重用等机制。本系列文章会包括到以下三个部分: 1. RecyclerView控件的基本使用,包括基础,进阶,高级部分,动画之类[(点击进入)](http://blog.csdn.net/developer_jiangqq/article/details/49927631) 2. RecyclerView控件的实战实例 3. RecyclerView控件集合AA(Android Annotations)注入框架实例 今天使我们本系列文章的第二讲主要是我们通过RecyclerView来打造一个新版类似Gallery控件的效果。本次讲解所有用的Demo例子已经全部更新到下面的项目中了,欢迎大家star和fork。 FastDev4Android框架项目地址:[https://github.com/jiangqqlmj/FastDev4Android](https://github.com/jiangqqlmj/FastDev4Android) ## (二).基本实现 上一讲我们已经对于RecyclerView的基本使用和进阶部分做了讲解[(点击进入)](http://blog.csdn.net/developer_jiangqq/article/details/49927631),下面我们一步步的来打造一个新版Gallery效果控件。先来看一下和RecyclerView相关类: | 类名| 说明| |--|--| | RecyclerView.Adapter|可以托管数据集合,为每一项Item创建视图并且绑定数据| | RecyclerView.ViewHolder| 承载Item视图的子布局| | RecyclerView.LayoutManager| 负责Item视图的布局的显示管理| | RecyclerView.ItemDecoration|给每一项Item视图添加子View,可以进行画分隔线之类的东西| | RecyclerView.ItemAnimator|负责处理数据添加或者删除时候的动画效果| 那如果要实现Gallery的效果,里面的Item是横向滑动的,也就是说我们的RecyclerView可以支持横向滑动,这边我们直接采用了LinearLayoutManager布局管理器,同时设置方向为:HORIZONTAL(水平) 下面来具体看代码: 1.作为RecyclerView控件,我们需要设置每一项Item的布局: ~~~ <?xmlversion="1.0" encoding="utf-8"?> <LinearLayoutxmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_height="wrap_content" android:layout_width="wrap_content" android:gravity="center" android:padding="8.0dip"> <ImageView android:id="@+id/item_img" android:layout_width="100dp" android:layout_height="100dp" android:scaleType="fitXY" android:adjustViewBounds="true" android:src="@drawable/ic_item_gallery"/> <TextView android:id="@+id/item_tv" android:text="标题1" android:layout_marginTop="5dp" android:textSize="15sp" android:layout_gravity="center_horizontal" android:layout_width="wrap_content" android:layout_height="wrap_content" /> </LinearLayout> ~~~ 这个布局中我们比较简单,定义了一个图片和一个标题,垂直方向布局。 2.间接着,和ListView写法差不多,需要自定义适配器,来创建每一项布局视图以及把数据和视图绑定起来,所以这边继承RecyclerView.Adapter类创建一个自定义适配器GalleryRecyclerAdapter.java。那么需要实现基类中的三个方法: * onCreateViewHolder(ViewGroup parent,int viewType) 创建Item View然后通过ViewHolder来承载 * onBindViewHolder(ViewHolder holder,int position)进行视图和数据绑定 * getItemCount()获取列表中视图Item的数量 具体GallerRecyclerAdapter实现代码如下: ~~~ public class GalleryRecyclerAdapter extends RecyclerView.Adapter<GalleryRecyclerAdapter.ViewHolder> { private List<GalleryModel> models; private LayoutInflater mInflater; public GalleryRecyclerAdapter(Context context){ models=new ArrayList<GalleryModel>(); for (int i=0;i<20;i++){ int index=i+1; models.add(new GalleryModel(R.drawable.ic_item_gallery,"Item"+index)); } mInflater=LayoutInflater.from(context); } /** * 创建Item View 然后使用ViewHolder来进行承载 * @param parent * @param viewType * @return */ @Override public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { View view=mInflater.inflate(R.layout.item_gallery_recycler,parent,false); ViewHolder viewHolder=new ViewHolder(view); return viewHolder; } /** * 进行绑定数据 * @param holder * @param position */ @Override public void onBindViewHolder(ViewHolder holder, int position) { holder.item_img.setImageResource(models.get(position).getImgurl()); holder.item_tv.setText(models.get(position).getTitle()); } @Override public int getItemCount() { return models.size(); } //自定义的ViewHolder,持有每个Item的的所有界面元素 public static class ViewHolder extends RecyclerView.ViewHolder { private ImageView item_img; private TextView item_tv; public ViewHolder(View view){ super(view); item_img=(ImageView)view.findViewById(R.id.item_img); item_tv=(TextView)view.findViewById(R.id.item_tv); } } } ~~~ 3.注意看上面的代码,我们继承了RecyclerView.ViewHolder实现一个自定义类ViewHolder,这个用来承载我们的子Item视图,现在Google已经要求开发者必须要使用ViewHolder了。在ViewHolder中我们进行控件的初始化工作,然后保存View视图。 ~~~ //自定义的ViewHolder,持有每个Item的的所有界面元素 public static class ViewHolder extends RecyclerView.ViewHolder { private ImageView item_img; private TextView item_tv; public ViewHolder(View view){ super(view); item_img=(ImageView)view.findViewById(R.id.item_img); item_tv=(TextView)view.findViewById(R.id.item_tv); } } ~~~ 4.最后在Activity中控件设置,例如布局管理器,Adapter绑定即可,完整代码如下: ~~~ public class RecyclerGalleryActivity extends BaseActivity { private RecyclerView gallery_recycler; private LinearLayout top_bar_linear_back; private TextView top_bar_title; @Override protected void onCreate(BundlesavedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.recycler_gallery_layout); top_bar_linear_back=(LinearLayout)this.findViewById(R.id.top_bar_linear_back); top_bar_linear_back.setOnClickListener(new CustomOnClickListener()); top_bar_title=(TextView)this.findViewById(R.id.top_bar_title); top_bar_title.setText("RecyclerView打造Gallery效果"); //初始化RecyclerView控件 gallery_recycler=(RecyclerView)this.findViewById(R.id.gallery_recycler); //固定高度 gallery_recycler.setHasFixedSize(true); //创建布局管理器 LinearLayoutManager linearLayoutManager=new LinearLayoutManager(this); //设置横向 linearLayoutManager.setOrientation(OrientationHelper.HORIZONTAL); //设置布局管理器 gallery_recycler.setLayoutManager(linearLayoutManager); //创建适配器 GalleryRecyclerAdapter adapter=new GalleryRecyclerAdapter(this); //绑定适配器 gallery_recycler.setAdapter(adapter); } class CustomOnClickListener implements View.OnClickListener{ @Override public void onClick(View v) { RecyclerGalleryActivity.this.finish(); } } } ~~~ 5.在看运行效果之前,我们先来看下上面的代码,上面的代码基本注释已经全部加了,相应大家可以看的懂,不过我们需要来讲一下上面的LayoutManager(布局管理器)。 在上一讲中我们也讲到了,LayoutManger(布局管理器)该类负责将每一个Item视图在RecyclerView中的布局。目前RecyclerView已经给我们提供三个内置管理器:LinearLayoutManger,GridLayoutManger以及StaggeredGridLayoutManager。这边的例子中我们是采用LinearLayoutManger而且设置了横向水平布局了。当然LinearLayoutManger还给我们提供了以下几个方法来让开发者方便的获取到屏幕上面的顶部item和顶部item相关的信息: * findFirstVisibleItemPosition() * findFirstCompletlyVisibleItemPosition() * findLastVisibleItemPosition() * findLastCompletlyVisibleItemPosition() 这边的具体设置代码如下: ~~~ //创建布局管理器 LinearLayoutManagerlinearLayoutManager=new LinearLayoutManager(this); //设置横向 linearLayoutManager.setOrientation(OrientationHelper.HORIZONTAL); //设置布局管理器 gallery_recycler.setLayoutManager(linearLayoutManager); ~~~ 6.初步运行效果如下: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569c8ebc9fc1e.jpg)  ## (三).升级加入点击事件 通过上面的方式我们显示了一个类似于Gallery的效果,但是还远远不如实际Gallery的效果,现在只是可以有多项Item以及可以左右滑动,但是没有点击事件,下面我们来加入点击事件操作。 对于ListView来讲,我们可以为ListView加入setOnItemClickListener监听事件,但是对于RecyclerView控件来讲,RecyclerView已经不再负载Item视图的布局和显示,这些工作已经交给了LayoutManger来做了。所以RecyclerView也没有给我们提供类似onItemClick事件,这样如果非得要实现类似的功能,我们开发者也可以自定义模拟实现。来,我们继续往下看…. 1.我们最终要实现点击列表上面每一项Item来回调点击方法,那么我们可以在Adapter中的每一项View上面做文章,首先我们来看一下Adapter中的onCreateViewHolder()方法: ~~~ public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { Viewview=mInflater.inflate(R.layout.item_gallery_recycler,parent,false); ViewHolder viewHolder=new ViewHolder(view); return viewHolder; } ~~~ 2.该方法创建出了Item 视图,然后通过ViewHolder来进行承载了,既然这样那我们可以在View加载出来之后给它设置一些属性例如:颜色,大小,当然也可以是点击事件等等。那这边我们给View添加onClick事件,然后在onClick方法把View点击触发的事件回调出去,同时可以回调一些参数内容出去。OK,那么我们这边就需要一个自定义的接口了,我们创建一个GallerRecyclerAdapter的内部类接口: ~~~ /** * 类似ListView的 onItemClickListener接口 */ public interface OnRecyclerViewItemClickListener{ /** * Item View发生点击回调的方法 * @param view 点击的View * @paramposition 具体Item View的索引 */ void onItemClick(View view,intposition); } ~~~ 3.然后定义接口,同时提供set和get方法,来让外部传入该接口,初始化: ~~~ private OnRecyclerViewItemClickListener onRecyclerViewItemClickListener; public OnRecyclerViewItemClickListener getOnRecyclerViewItemClickListener() { return onRecyclerViewItemClickListener; } public void setOnRecyclerViewItemClickListener(OnRecyclerViewItemClickListener onRecyclerViewItemClickListener) { this.onRecyclerViewItemClickListener =onRecyclerViewItemClickListener; } ~~~ 4.现在开始在onCreateViewHolder()方法中给View添加一个onClick事件,然后相应处理,判断onRecyclerViewItemClickListener是否存在,把事件回调出去: ~~~ view.setOnClickListener(newView.OnClickListener() { @Override public void onClick(View v) { if(onRecyclerViewItemClickListener!=null){ onRecyclerViewItemClickListener.onItemClick(view,(int)view.getTag()); } } }); ~~~ 5.上面的代码中大家可能注意到onItemClick()方法中的第二个参数,获取了tag,因为这边的position索引值是在onBindViewHolder()方法中设置的: ~~~ public voidonBindViewHolder(ViewHolder holder, int position) { holder.item_img.setImageResource(models.get(position).getImgurl()); holder.item_tv.setText(models.get(position).getTitle()); holder.itemView.setTag(position); } ~~~ 6.OK这边我们搞定了一个Item点击监听方法,接下去就是使用了, ~~~ adapter.setOnRecyclerViewItemClickListener(new GalleryRecyclerAdapter.OnRecyclerViewItemClickListener() { @Override public void onItemClick(View view,int position) { Toast.makeText(RecyclerGalleryActivity.this,"您点击的Item的索引为:"+position,Toast.LENGTH_SHORT).show(); } }); ~~~ 7.现在该功能代码整完了,运行效果如下: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569c8ebd02931.jpg) ## (四).升级之加入分割线 上面我们已经给每一项Item加入了点击回调事件,但是总感觉还缺少点什么东西,例如分隔线。很遗憾的是,RecyclerView没有提供ListView控件这样设置分割线的方法,不过它给我们提供了ItemDecoration类。这个ItemDecoration可以使得每一个Item在视觉上面进行分隔开来。RecyclerView没有要求ItemDecoration必须要设置,同样作为开发者可以选择不设置或者设置多个Decoration。然后RecyclerView会进行相应的绘制。 我们这边定义了一个TestDecoration类,该类继承自RecyclerView.Decoration。只需要实现一下的两个方法即可: * onDraw(Canvas c,RecyclerView parent,RecyclerView.State state) * getItemOffset(Rect outRect,int itemPosition,RecyclerView parent) 具体实现代码如下: ~~~ public class TestDecoration extends RecyclerView.ItemDecoration { //采用系统内置的风格的分割线 private static final int[] attrs=newint[]{android.R.attr.listDivider}; private Drawable mDivider; public TestDecoration(Context context) { TypedArray typedArray=context.obtainStyledAttributes(attrs); mDivider=typedArray.getDrawable(0); typedArray.recycle(); } /** * 进行自定义绘制 * @param c * @param parent * @param state */ @Override public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) { int top=parent.getPaddingTop(); intbottom=parent.getHeight()-parent.getPaddingBottom(); int childCount=parent.getChildCount(); for(int i=0;i<childCount;i++){ View child=parent.getChildAt(i); RecyclerView.LayoutParams layoutParams=(RecyclerView.LayoutParams)child.getLayoutParams(); intleft=child.getRight()+layoutParams.rightMargin; intright=left+mDivider.getIntrinsicWidth(); mDivider.setBounds(left,top,right,bottom); mDivider.draw(c); } } @Override public void getItemOffsets(Rect outRect,View view, RecyclerView parent, RecyclerView.State state) { outRect.set(0,0,mDivider.getIntrinsicWidth(),0); } } ~~~ 最后给RecyclerView添加该分隔线即可: ~~~ //设置分割线 gallery_recycler.addItemDecoration(new TestDecoration(this)); ~~~ 运行效果如下: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569c8ebd6ad58.jpg) ## (五).升级之分割线改造 仔细看上面的运行效果,我们会发现一个问题,那就是分割线垂直分布,但是没有自适应控件的高度,直接延伸到界面的底部了。重新检查了有关的所有布局文件发现,高度都设置成了warp_content,但是实际的效果还是没有自适应。原来在哪里呢? 真正的原因是因为RecyclerView控件已经不负责每一项VIew的显示了,那我们来看LayoutManger(布局管理器)该进行负责Item的布局显示了,所以我们需要进行实现一个LayoutManger,然后重写里边的onMeasure()方法。计算高度即可,具体代码如下: ~~~ public class CustomLinearLayoutManager extends LinearLayoutManager { public CustomLinearLayoutManager(Contextcontext) { super(context); } @Override public void onMeasure(RecyclerView.Recycler recycler, RecyclerView.State state, int widthSpec, int heightSpec) { Viewview=recycler.getViewForPosition(0); if(view!=null){ measureChild(view,widthSpec,heightSpec); int mWidth=View.MeasureSpec.getSize(widthSpec); intmHeight=view.getMeasuredHeight(); setMeasuredDimension(mWidth,mHeight); } } } ~~~ 然后RecyclerView使用CustomLinearLayoutManger即可,运行效果如下: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569c8ebdb4634.jpg) ## (六).升级之添加删除Item动画 RecyclerView控件的一个优美之处就是当里边Item发生变化的时候可以加入相应的动画效果,涉及的类为RecyclerView.ItemAnimatior。一般当存在以下三种操作的时候可以加入动画效果: * Item删除 * Item添加 * Item移动 当我们的数据,或者移动的时候,去掉用Adapter给我们提供的以下两个方法即可: *  notifyItemInserted(int position) * notifyItemRemoved(int position) 那我们可以在Adapter中加入两个方法,分别为添加Item和删除Item的方法: ~~~ //添加数据 public void addItem(GalleryModel model, intposition) { models.add(position, model); notifyItemInserted(position); } //删除数据 public void removeItem(int position) { models.remove(position); notifyItemRemoved(position); } ~~~ 然后在外部进行调用这两个方法: ~~~ adapter.addItem(newGalleryModel(R.drawable.ic_item_gallery,"Item Add"),3); adapter.removeItem(2); ~~~ 最后千万不要忘记给RecyclerView设置动画效果,我这边就直接采用默认动画了。 ~~~ //设置动画 gallery_recycler.setItemAnimator(newDefaultItemAnimator()); ~~~  最终运行效果如下: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569c8ebdd2757.jpg) ## (七).最后总结 今天通过实例带大家又重新把RecyclerView的相关使用讲解了一遍,实现类似Gallery效果,当然实例中还有很多缺点,需要进一步优化,后面的文章中也会继续更新的~ 本次具体实例注释过的全部代码已经上传到FastDev4Android项目中了。同时欢迎大家去Github站点进行clone或者下载浏览: [https://github.com/jiangqqlmj/FastDev4Android](https://github.com/jiangqqlmj/FastDev4Android) 同时欢迎大家star和fork整个开源快速开发框架项目~下一讲我们会进行RecyclerView集合AA(Android Annotations)注入框架来实现实例,敬请期待!
';

RecyclerView完全解析,让你从此爱上它(二十八)

最后更新于:2022-04-01 07:13:22

## (一).前言: 话说RecyclerView已经面市很久,也在很多应用中得到广泛的使用,在整个开发者圈子里面也拥有很不错的口碑,那说明RecyclerView拥有比ListView,GridView之类控件有很多的优点,例如:数据绑定,Item View创建,View的回收以及重用等机制。那么今天开始我们来重点学习一下RecyclerView控件,本系列文章会包括到以下三个部分: 1. RecyclerView控件的基本使用,包括基础,进阶,高级部分,动画之类 2. RecyclerView控件的实战实例 3. RecyclerView控件集合AA(Android Annotations)注入框架实例 那么今天我们首先来看第一部分:RecyclerView控件的基本使用,进阶,动画相关知识点。本次讲解所有用的Demo例子已经全部更新到下面的项目中了,欢迎大家star和fork。 FastDev4Android框架项目地址:[https://github.com/jiangqqlmj/FastDev4Android](https://github.com/jiangqqlmj/FastDev4Android) ## (二).RecyclerView基本介绍: 通过使用RecyclerView控件,我们可以在APP中创建带有Material Design风格的复杂列表。RecyclerView控件和ListView的原理有很多相似的地方,都是维护少量的View来进行显示大量的数据,不过RecyclerView控件比ListView更加高级并且更加灵活。当我们的数据因为用户事件或者网络事件发生改变的时候也能很好的进行显示。和ListView不同的是,RecyclerView不用在负责Item的显示相关的功能,在这边所有有关布局,绘制,数据绑定等都被分拆成不同的类进行管理,下面我这边会一个个的进行讲解。同时RecyclerView控件提供了以下两种方法来进行简化和处理大数量集合: 1. 采用LayoutManager来处理Item的布局 2. 提供Item操作的默认动画,例如在增加或者删除item的时候 你也可以自定义LayoutManager或者设置添加/删除的动画,整体的RecyclerView结构图如下: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569c8ebae4667.jpg) 为了使用RecyclerView控件,我们需要创建一个Adapter和一个LayoutManager: Adapter:继承自RecyclerView.Adapetr类,主要用来将数据和布局item进行绑定。 LayoutManager:布局管理器,设置每一项view在RecyclerView中的位置布局以及控件item view的显      示或者隐藏.当View重用或者回收的时候,LayoutManger都会向Adapter来请求新的数据来进行替换原来数据的内容。这种回收重用的机制可以提供性能,避免创建很多的view或者是频繁的调用findViewById方法。这种机制和ListView还是很相似的。 RecyclerView提供了三种内置的LayoutManager: 1. LinearLayoutManager:线性布局,横向或者纵向滑动列表 2. GridLayoutManager:表格布局 3. StaggeredGridLayoutManager:流式布局,例如瀑布流效果 当然除了上面的三种内部布局之外,我们还可以继承RecyclerView.LayoutManager来实现一个自定义的LayoutManager。 Animations(动画)效果: RecyclerView对于Item的添加和删除是默认开启动画的。我们当然也可以通过RecyclerView.ItemAnimator类定制动画,然后通过RecyclerView.setItemAnimator()方法来进行使用。 RecyclerView相关类: | 类名| 说明| |--|--| | RecyclerView.Adapter|可以托管数据集合,为每一项Item创建视图并且绑定数据| | RecyclerView.ViewHolder|承载Item视图的子布局| | RecyclerView.LayoutManager| 负责Item视图的布局的显示管理| | RecyclerView.ItemDecoration|给每一项Item视图添加子View,例如可以进行画分隔线之类| | RecyclerView.ItemAnimator|负责处理数据添加或者删除时候的动画效果| ## (三).RecyclerView基本实现:  我这边实例采用Android Studio 1.3.2。 1.添加库依赖: ~~~ dependencies { ……. compile'com.android.support:recyclerview-v7:23.1.1' } ~~~ 2.新建布局,引入RecyclerView控件: ~~~ <?xmlversion="1.0" encoding="utf-8"?> <LinearLayoutxmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical"android:layout_width="match_parent" android:layout_height="match_parent"> <includelayout="@layout/common_top_bar_layout"/> <android.support.v7.widget.RecyclerView android:id="@+id/recyclerView_one" android:layout_width="match_parent" android:layout_height="match_parent" android:scrollbars="vertical" ></android.support.v7.widget.RecyclerView> </LinearLayout> ~~~ 3.在Activity中获取RecyclerView控件然后进行设置LayoutManger以及Adapter即可,和ListView的写法有点类似: ~~~ /** * 当前类注释:RecyclerView使用实例测试demo * 项目名:FastDev4Android * 包名:com.chinaztt.fda.test * 作者:江清清 on 15/11/17 15:10 * 邮箱:jiangqqlmj@163.com * QQ: 781931404 * 公司:江苏中天科技软件技术有限公司 */ public class RecyclerViewTestActivity extends BaseActivity { private LinearLayout top_bar_linear_back; private TextView top_bar_title; private RecyclerView recyclerView_one; private RecyclerView.Adapter mAdapter; private LinearLayoutManager mLayoutManager; @Override protected void onCreate(BundlesavedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.recyclerview_test_layout); top_bar_linear_back=(LinearLayout)this.findViewById(R.id.top_bar_linear_back); top_bar_linear_back.setOnClickListener(new CustomOnClickListener()); top_bar_title=(TextView)this.findViewById(R.id.top_bar_title); top_bar_title.setText("RecyclerView使用实例"); //开始设置RecyclerView recyclerView_one=(RecyclerView)this.findViewById(R.id.recyclerView_one); //设置固定大小 recyclerView_one.setHasFixedSize(true); //创建线性布局 mLayoutManager = newLinearLayoutManager(this); //垂直方向 mLayoutManager.setOrientation(OrientationHelper.VERTICAL); //给RecyclerView设置布局管理器 recyclerView_one.setLayoutManager(mLayoutManager); //创建适配器,并且设置 mAdapter = newTestRecyclerAdapter(this); recyclerView_one.setAdapter(mAdapter); } class CustomOnClickListener implements View.OnClickListener{ @Override public void onClick(View v) { RecyclerViewTestActivity.this.finish(); } } } ~~~ 4.自定义一个适配器来进行创建item view以及绑定数据 ~~~ /** * 当前类注释:RecyclerView 数据自定义Adapter * 项目名:FastDev4Android * 包名:com.chinaztt.fda.adapter.base * 作者:江清清 on 15/11/18 22:29 * 邮箱:jiangqqlmj@163.com * QQ: 781931404 * 公司:江苏中天科技软件技术有限公司 */ public class TestRecyclerAdapter extends RecyclerView.Adapter<TestRecyclerAdapter.ViewHolder>{ private LayoutInflater mInflater; private String[] mTitles=null; public TestRecyclerAdapter(Context context){ this.mInflater=LayoutInflater.from(context); this.mTitles=new String[20]; for (int i=0;i<20;i++){ int index=i+1; mTitles[i]="item"+index; } } /** * item显示类型 * @param parent * @param viewType * @return */ @Override public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { Viewview=mInflater.inflate(R.layout.item_recycler_layout,parent,false); //view.setBackgroundColor(Color.RED); ViewHolder viewHolder=new ViewHolder(view); return viewHolder; } /** * 数据的绑定显示 * @param holder * @param position */ @Override public void onBindViewHolder(ViewHolder holder, int position) { holder.item_tv.setText(mTitles[position]); } @Override public int getItemCount() { return mTitles.length; } //自定义的ViewHolder,持有每个Item的的所有界面元素 public static class ViewHolder extends RecyclerView.ViewHolder { public TextView item_tv; public ViewHolder(View view){ super(view); item_tv = (TextView)view.findViewById(R.id.item_tv); } } ~~~ 这个自定义Adapter和我们在使用Listview时候的Adapter相比还是有点不太一样的,首先这边我们需要继承RecyclerView.Adaper类,然后实现两个重要的方法onBindViewHodler()以及onCreateViewHolder(),这边我们看出来区别,使用RecyclerView控件我们就可以把Item View视图创建和数据绑定这两步进行分来进行管理,用法就更加方便而且灵活了,并且我们可以定制打造千变万化的布局。同时这边我们还需要创建一个ViewHolder类,该类必须继承自RecyclerView.ViewHolder类,现在Google也要求我们必须要实现ViewHolder来承载Item的视图。 该Demo运行效果如下: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569c8ebb1ab5c.jpg) 上面的例子我们这边比较简单使用LinearLayoutManager来实现了,其中布局是采用垂直布局的,当然我们还可以设置线性布局的方向为横向,只要如下设置即可: ~~~ mLayoutManager.setOrientation(OrientationHelper.HORIZONTAL); ~~~ 运行效果如下: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569c8ebb7cbfd.jpg) 那么另外两种内置的布局如下: 1.GridLayoutManger:使用如下设置: ~~~ GridLayoutManager girdLayoutManager=new GridLayoutManager(this,4); recyclerView_one.setLayoutManager(girdLayoutManager); ~~~ 运行效果如下: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569c8ebbaa18a.jpg) 2.GridLayoutManger:使用如下设置: ~~~ StaggeredGridLayoutManager staggeredGridLayoutManager=new StaggeredGridLayoutManager(2,OrientationHelper.VERTICAL); recyclerView_one.setLayoutManager(staggeredGridLayoutManager); ~~~ ## (四).RecyclerView分隔线实现(ItemDecoration): 大家肯定观察到上面的显示效果还是比较丑,例如就没有分隔线这个效果,下面我们一起来实现以下分隔线的效果。还记得前面的一个表格中有写关于RecyclerView的相关类: | RecyclerView.ItemDecoration|给每一项Item视图添加子View,可以进行画分隔线之类的东西| |--|--| 我们可以创建一个继承RecyclerView.ItemDecoration类来绘制分隔线,通过ItemDecoration可以让我们每一个Item从视觉上面相互分开来,例如ListView的divider非常相似的效果。当然像我们上面的例子ItemDecoration我们没有设置也没有报错哦,那说明ItemDecoration我们并不是强制需要使用,作为我们开发者可以设置或者不设置Decoration的。实现一个ItemDecoration,系统提供的ItemDecoration是一个抽象类,内部除去已经废弃的方法以外,我们主要实现以下三个方法: ~~~ public static abstract class ItemDecoration { public void onDraw(Canvas c,RecyclerView parent, State state) { onDraw(c, parent); } public void onDrawOver(Canvas c,RecyclerView parent, State state) { onDrawOver(c, parent); } public void getItemOffsets(RectoutRect, View view, RecyclerView parent, State state) { getItemOffsets(outRect,((LayoutParams) view.getLayoutParams()).getViewLayoutPosition(), parent); } } ~~~ 又因为当我们RecyclerView在进行绘制的时候会进行绘制Decoration,那么会去调用onDraw和onDrawOver方法,那么这边我们其实只要去重写onDraw和getItemOffsets这两个方法就可以实现啦。然后LayoutManager会进行Item布局的时候,回去调用getItemOffset方法来计算每个Item的Decoration合适的尺寸,下面我们来具体实现一个Decoration。TestDecoration.java ~~~ /** * 当前类注释:自定义实现一个Decoration分隔线 * 项目名:FastDev4Android * 包名:com.chinaztt.fda.widget * 作者:江清清 on 15/11/19 12:29 * 邮箱:jiangqqlmj@163.com * QQ: 781931404 * 公司:江苏中天科技软件技术有限公司 */ public class TestDecoration extends RecyclerView.ItemDecoration { //采用系统内置的风格的分割线 private static final int[] attrs=newint[]{android.R.attr.listDivider}; private Drawable mDivider; public TestDecoration(Context context) { TypedArray typedArray=context.obtainStyledAttributes(attrs); mDivider=typedArray.getDrawable(0); } /** * 进行自定义绘制 * @param c * @param parent * @param state */ @Override public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) { int top=parent.getPaddingTop(); intbottom=parent.getHeight()-parent.getPaddingBottom(); int childCount=parent.getChildCount(); for(int i=0;i<childCount;i++){ View child=parent.getChildAt(i); RecyclerView.LayoutParams layoutParams=(RecyclerView.LayoutParams)child.getLayoutParams(); intleft=child.getRight()+layoutParams.rightMargin; intright=left+mDivider.getIntrinsicWidth(); mDivider.setBounds(left,top,right,bottom); mDivider.draw(c); } } @Override public void getItemOffsets(Rect outRect,View view, RecyclerView parent, RecyclerView.State state) { outRect.set(0,0,mDivider.getIntrinsicWidth(),0); } } ~~~ 我这边实例中采用系统主题(android.R.attr.listDivider)来设置成分隔线的,然后来获取尺寸,位置进行setBound(),绘制,接着通过outRect.set()来设置绘制整个区域范围,最后不要忘记往RecyclerView中设置该自定义的分割线, //添加分隔线 ~~~ recyclerView_one.addItemDecoration(newTestDecoration(this)); ~~~ 运行效果如下: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569c8ebbbb567.jpg) 上面的分割线效果只是垂直画了分割线,但是我们水平方向也要进行画分割线,那么我们下面对于自定义的Decoration进行改进,AdvanceDecoration.java就出现啦。 ~~~ /** * 当前类注释:改进之后的自定义Decoration分割线 * 项目名:FastDev4Android * 包名:com.chinaztt.fda.widget * 作者:江清清 on 15/11/19 12:53 * 邮箱:jiangqqlmj@163.com * QQ: 781931404 * 公司:江苏中天科技软件技术有限公司 */ public class AdvanceDecoration extends RecyclerView.ItemDecoration{ //采用系统内置的风格的分割线 private static final int[] attrs=newint[]{android.R.attr.listDivider}; private Drawable mDivider; private int orientation; public AdvanceDecoration(Contextcontext,int orientation) { TypedArray typedArray=context.obtainStyledAttributes(attrs); mDivider=typedArray.getDrawable(0); typedArray.recycle(); this.orientation=orientation; } @Override public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) { drawHDeraction(c,parent); drawVDeraction(c,parent); } /** * 绘制水平方向的分割线 * @param c * @param parent */ private void drawHDeraction(Canvas c,RecyclerView parent){ int left=parent.getPaddingLeft(); intright=parent.getWidth()-parent.getPaddingRight(); int childCount=parent.getChildCount(); for(int i=0;i<childCount;i++){ View child=parent.getChildAt(i); RecyclerView.LayoutParams layoutParams=(RecyclerView.LayoutParams)child.getLayoutParams(); inttop=child.getBottom()+layoutParams.bottomMargin; intbottom=top+mDivider.getIntrinsicHeight(); mDivider.setBounds(left,top,right,bottom); mDivider.draw(c); } } /** * 绘制垂直方向的分割线 * @param c * @param parent */ private void drawVDeraction(Canvas c,RecyclerView parent){ int top=parent.getPaddingTop(); intbottom=parent.getHeight()-parent.getPaddingBottom(); int childCount=parent.getChildCount(); for(int i=0;i<childCount;i++){ View child=parent.getChildAt(i); RecyclerView.LayoutParamsla youtParams=(RecyclerView.LayoutParams)child.getLayoutParams(); intleft=child.getRight()+layoutParams.rightMargin; intright=left+mDivider.getIntrinsicWidth(); mDivider.setBounds(left,top,right,bottom); mDivider.draw(c); } } @Override public void getItemOffsets(Rect outRect,View view, RecyclerView parent, RecyclerView.State state) { if(OrientationHelper.HORIZONTAL==orientation){ outRect.set(0, 0,mDivider.getIntrinsicWidth(), 0); }else { outRect.set(0, 0, 0,mDivider.getIntrinsicHeight()); } } } ~~~ 改良之后的自定义分割器的构造函数中新增一个int参数,用来表示横向还是纵向布局,这样我们可以分别来绘制分割线。具体使用方法: recyclerView_one.addItemDecoration(newAdvanceDecoration(this,OrientationHelper.VERTICAL)); 运行比较效果如下: 1.横向 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569c8ebbbb567.jpg) 2.纵向 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569c8ebbd271b.jpg) ## (五).RecyclerView高级用户(监听事件处理) 我们知道在ListView使用的时候,该控件给我们提供一个onItemClickListener监听器,这样当我们的item发生触发事件的时候,会回调相关的方法,以便我们方便处理Item点击事件。对于RecyclerView来讲,非常可惜的时候,该控件没有给我们提供这样的内置监听器方法,不过我们可以进行改造实现。我们先来看一下之前我们写得TestRecyclerAdapter中的onCreateViewHolder()方法中的代码: ~~~ public ViewHolder onCreateViewHolder(ViewGroupparent, int viewType) { Viewview=mInflater.inflate(R.layout.item_recycler_layout,parent,false); //这边可以做一些属性设置,甚至事件监听绑定 //view.setBackgroundColor(Color.RED); ViewHolder viewHolder=newViewHolder(view); return viewHolder; } ~~~ 该方法创建一个ViewHolder,其中承载的就是每一项Item View视图,那么我们可以在view创建出来之后给它进行添加相应的属性或者监听方法,例如:背景颜色,大小,以及点击事件。既然可以这样解决,OK,我们给View添加一个onClickListener监听器,然后点击的时候回调onClick()方法。同时我们需要自定义一个类似于onItemClickListener()的监听器来处理。 ~~~ /** * 自定义RecyclerView 中item view点击回调方法 */ interface OnRecyclerItemClickListener{ /** * item view 回调方法 * @param view 被点击的view * @param position 点击索引 */ void onItemClick(View view, intposition); } ~~~ 然后声明以及Adapter初始化的时候传入进去: ~~~ public TestRecyclerAdapter(Contextcontext,OnRecyclerItemClickListener onRecyclerItemClickListener){ …….. this.onRecyclerItemClickListener=onRecyclerItemClickListener; } ~~~ 然后我们在onClick回调方法中调用OnRecyclerItemClickListener接口的方法。 ~~~ view.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if(onRecyclerItemClickListener!=null){ onRecyclerItemClickListener.onItemClick(view, (int)view.getTag()); } } }); ~~~ 上面的onItemClick中第二个参数的position,采用view.getTag的方法获取,那么我们就需要在onBindViewHolder()方法中设置一个tag了. ~~~ public voidonBindViewHolder(ViewHolder holder, int position) { holder.item_tv.setText(mTitles[position]); holder.itemView.setTag(position); } ~~~ 最后我们在外部使用一下接口:  ~~~ mAdapter = new TestRecyclerAdapter(this, new TestRecyclerAdapter.OnRecyclerItemClickListener() { @Override public void onItemClick(View view,int position) { Toast.makeText(RecyclerViewTestActivity.this, "点击了第"+position+"项", Toast.LENGTH_SHORT).show(); } }); ~~~ 运行结果如下: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569c8ebbe94c3.jpg) ## (六).RecyclerView数据添加删除处理 讲了以上RecyclerView中各种用户,处理之后,现在我们来看一下当数据发生变化之后的处理。例如我们在使用ListView的时候,当数据发生变化的时候可以通过notifyDatasetChange()来刷新界面。对于RecyclerView控件来讲,给我们提供更加高级的使用方法notifyItemInserted(position)和notifyItemRemoved(position) 我们可以在TestRecyclerAdapter中添加数据新增和数据删除的方法如下: ~~~ //添加数据 public void addItem(String data, intposition) { mTitles.add(position, data); notifyItemInserted(position); } //删除数据 public void removeItem(String data) { int position = mTitles.indexOf(data); mTitles.remove(position); notifyItemRemoved(position); } ~~~ 然后我们在Activity中进行调用即可: ~~~ //添加数据 mAdapter.addItem("additem",5); //删除数据 mAdapter.removeItem("item4"); ~~~ 在运行之前我们不要忘记RecyclerView给提供了动画设置,我这边就直接采用了默认动画,设置方法如下: ~~~ //添加默认的动画效果 recyclerView_one.setItemAnimator(new DefaultItemAnimator()); ~~~ 最终运行效果如下图: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569c8ebc307f7.jpg) ## (七).RecyclerView总结 到此为止就完成我们RecyclerView控件使用的第一讲内容,其中包括控件的基本介绍,基本使用,高级用法(自定义间隔符,加入点击监听事件以及Item添加删除动画处理)。总体来讲RecyclerView控件是非常不错,尤其在布局以及数据绑定,动画方面,除了系统内置的三种布局方式之外,我们还可以定制出我们自己的布局管理器。同时当item数据发生变化的时候还给我们提供非常炫的效果。相信大家在今天这一讲之后,会越来越爱上RecyclerView控件的使用,从此可以抛弃ListView和GridView啦. 本次具体实例注释过的全部代码已经上传到FastDev4Android项目中了。同时欢迎大家去Github站点进行clone或者下载浏览: [https://github.com/jiangqqlmj/FastDev4Android](https://github.com/jiangqqlmj/FastDev4Android) 同时欢迎大家star和fork整个开源快速开发框架项目~下一讲我们会通过一个具体实例来自定义实现一个广告条控件实例。 本人录制AA(Android Annotations)注入框架的视频教程已经上线了,欢迎大家前往观看。[http://www.cniao5.com/course/10074](http://www.cniao5.com/course/10074) [![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569c8ebc8324c.jpg)](http://www.cniao5.com/course/10074)
';

Volley完全解析之进阶最佳实践与二次封装(二十七)

最后更新于:2022-04-01 07:13:19

## (一).前言: 上一讲我们已经对Volley使用基础阶段涉及到字符串,JSON,图片等等网络数据请求相关方法的使用。今天我们对于Volley框架来一个进阶使用扩展封装,以及对上一篇遗留的问题做一下具体修改使用。主要涉及新增GsonRequest,ImageLoader列表图片加载,ImageCache,Volley框架StringRequest二次封装以及post请求新增设置请求参数方法。 FastDev4Android框架项目地址:[https://github.com/jiangqqlmj/FastDev4Android](https://github.com/jiangqqlmj/FastDev4Android) ## (二).post请求扩展请求参数方法 还记得前面文章我们只是使用Request的GET请求方法,当然作为完善的网络框架POST请求必须支持,不过很可惜的是Volley框架没有给我们显示的提供设置POST请求参数的方法,不过我们有两种方法来进行设置。不过在做之前我们需要先从源码角度来分析一下。 查看源码,我们发现Volley请求最终会被封装成HttpRequest子类对象HttpUrlRequest,该方法在HttpClientStack中的createHttpRequest()方法,代码和相关注释如下: ~~~ /** * 进行创建httprequest,这边获取httprequest的子类,httpurlrequest * Creates the appropriate subclass ofHttpUriRequest for passed in request. */ @SuppressWarnings("deprecation") /* protected */ static HttpUriRequest createHttpRequest(Request<?> request, Map<String, String>additionalHeaders) throws AuthFailureError { switch (request.getMethod()) { case Method.DEPRECATED_GET_OR_POST:{ // This is the deprecated waythat needs to be handled for backwards compatibility. // If the request's post bodyis null, then the assumption is that the request is // GET. Otherwise, it is assumed that the request isa POST. byte[] postBody =request.getPostBody(); if (postBody != null) { HttpPost postRequest = new HttpPost(request.getUrl()); postRequest.addHeader(HEADER_CONTENT_TYPE,request.getPostBodyContentType()); HttpEntity entity; entity = new ByteArrayEntity(postBody); postRequest.setEntity(entity); return postRequest; } else { return newHttpGet(request.getUrl()); } } case Method.GET: return newHttpGet(request.getUrl()); case Method.DELETE: return newHttpDelete(request.getUrl()); case Method.POST: { HttpPost postRequest = new HttpPost(request.getUrl()); postRequest.addHeader(HEADER_CONTENT_TYPE,request.getBodyContentType()); //设置请求体 body信息 -如果请求体不为空---[注意] setEntityIfNonEmptyBody(postRequest, request); return postRequest; } case Method.PUT: { HttpPut putRequest = new HttpPut(request.getUrl()); putRequest.addHeader(HEADER_CONTENT_TYPE, request.getBodyContentType()); setEntityIfNonEmptyBody(putRequest, request); return putRequest; } case Method.HEAD: return new HttpHead(request.getUrl()); case Method.OPTIONS: return new HttpOptions(request.getUrl()); case Method.TRACE: return new HttpTrace(request.getUrl()); case Method.PATCH: { HttpPatch patchRequest = newHttpPatch(request.getUrl()); patchRequest.addHeader(HEADER_CONTENT_TYPE,request.getBodyContentType()); setEntityIfNonEmptyBody(patchRequest, request); return patchRequest; } default: throw newIllegalStateException("Unknown request method."); } } ~~~ 注意查看以上caseMethod.POST部分中的setEntityIfNonEmptyBody();中进行设置请求体相关数据: ~~~ /** * 如果request的请求体不为空,进行设置请求体信息 * @param httpRequest * @param request * @throws AuthFailureError */ private static void setEntityIfNonEmptyBody(HttpEntityEnclosingRequestBase httpRequest, Request<?> request) throws AuthFailureError { byte[] body = request.getBody(); if (body != null) { HttpEntity entity = new ByteArrayEntity(body); httpRequest.setEntity(entity); } } ~~~ 然后会执行Request中getBody()方法来获取请求体。 ~~~ /** * Returns the raw POST or PUT body to besent. * 如果请求是POST或者PUT方法,去获取请求参数信息,然后设置到请求中 * <p>By default, the body consistsof the request parameters in * application/x-www-form-urlencodedformat. When overriding this method, consider overriding * {@link #getBodyContentType()} as well tomatch the new body format. * * @throws AuthFailureError in the event ofauth failure */ public byte[] getBody() throws AuthFailureError { //获取请求参数信息 Map<String, String> params =getParams(); if (params != null &¶ms.size() > 0) { return encodeParameters(params,getParamsEncoding()); } return null; } ~~~ 最后会获取XxxRequest的基类Requst类中的getParams()来获取请求参数信息,然后设置进去。 OK这个设置POST请求参数信息的流程大家应该能清楚吧。但是Volley源代码这边就直接返回null,如下: ~~~ protected Map<String, String> getParams() throws AuthFailureError { return null; } ~~~ 又因为Volley是开源的,那么下面我们开源对Request和XxxRequest类进行改造修改源代码,往外提供POST请求参数设置的方法,修改步骤如下: 2.1.Request类一个Map对象mParams,然后添加set方法,然后在getParams()进行return该mParams。具体修改如下: ~~~ /** * 自定义修改 新增POST请求参数map */ protected Map<String,String> mParams=null; public void setParams(Map<String,String> params) { this.mParams = params; } ~~~ ~~~ /** * 进行获取post请求参数数据 * Returns a Map of parameters to be usedfor a POST or PUT request. Can throw * {@link AuthFailureError} asauthentication may be required to provide these values. * * <p>Note that you can directlyoverride {@link #getBody()} for custom data.</p> * * @throws AuthFailureError in the event ofauth failure */ protected Map<String, String> getParams() throws AuthFailureError { //return null; //重新进行返回params return mParams; } ~~~ 2.2.间接着我们对StringRequest类进行扩展一个构造方法,加入请求参数Map对象,具体代码如下: ~~~ /** * 扩展POST请求构造函数 * @param url 请求地址 * @param listener 数据请求加载成功监听器 * @param errorListener 数据请求加载失败监听器 * @param params POST请求参数 */ public StringRequest(String url,Listener<String> listener, ErrorListenererrorListener,Map<String,String> params) { this(Method.POST, url, listener,errorListener); //进行初始化Request中post 请求参数 setParams(params); } ~~~ OK这两步修改就完成了POST请求参数设置扩展,下面我们来一看一下具体使用方法: ~~~ RequestQueue requestQueue=Volley.newRequestQueue(this); //修改Volley源代码,扩展StringRequest支持post参数设置 Map<String,String>params=new HashMap<String,String>(); params.put("username","zhangsan"); params.put("password","12345"); StringRequest post_stringRequest=new StringRequest("http://10.18.3.123:8080/SalesWebTest/TestVolleyPost",new Response.Listener<String>() { @Override public void onResponse(String response) { tv_result.setVisibility(View.VISIBLE); img_result.setVisibility(View.GONE); tv_result.setText(response.toString()); } }, new Response.ErrorListener(){ @Override public void onErrorResponse(VolleyError error) { } },params); requestQueue.add(post_stringRequest) ~~~ 这样就完成POST请求参数设置的扩展,另外的一种方案是创建StringRequest对象的时候实匿名类中重写getParams()方法,然后构造Map对象设置参数信息return该对象即可。不过个人觉的这种方案不推荐还是使用设置的方法比较好。 ## (三).Imageloader和ImageCache 在前面使用ImageLoder对象加载图片的时候,有一个优点我们没有深入讲解,因为ImageLoader是支持缓存功能,所以我们在创建ImageLoader需要传入一个缓存管理器,里面传入我们自定义的缓存策略,Fdv_ImageCache我们这边引入LruCache算法具体实现如下: ~~~ packagecom.chinaztt.fdv; importandroid.graphics.Bitmap; importandroid.util.LruCache; importcom.android.volley.toolbox.ImageLoader; /** * 当前类注释:图片缓存器,实现ImageLoder.ImageCache实现其中的方法,具体图片怎么样缓存让我们自己来实现 * 这样可以考虑到将来的扩展性 * 项目名:FastDev4Android * 包名:com.chinaztt.fdv * 作者:江清清 on 15/11/12 12:31 * 邮箱:jiangqqlmj@163.com * QQ: 781931404 * 公司:江苏中天科技软件技术有限公司 */ public class Fdv_ImageCache implements ImageLoader.ImageCache { private LruCache<String, Bitmap>mCache=null; private static final intCACHE_MAX_SIZE = 8 * 1024 * 1024; //默认缓存大小为8M public Fdv_ImageCache(){ if(mCache==null){ mCache = new LruCache<String,Bitmap>(CACHE_MAX_SIZE) { @Override protected int sizeOf(Stringkey, Bitmap bitmap) { return bitmap.getRowBytes()* bitmap.getHeight(); } }; } } /** * 从缓存中获取图片 * @param url 获取图片key 当然该key可以根据实际情况 使用url进行变换修改 * @return */ @Override public Bitmap getBitmap(String url) { return mCache.get(url); } /** * 向缓存中添加图片 * @param url 缓存图片key,当然该key可以根据实际情况 使用url进行变换修改 不过规格需要和上面方法的key保持一致 * @param bitmap 需要缓存的图片 */ @Override public void putBitmap(String url, Bitmapbitmap) { mCache.put(url,bitmap); } } ~~~ 有了图片缓存机制,然后我们在配合ImageLoader来进行列表上面加载异步图片的实现,具体实现方式如下: ~~~ /** * 当前类注释:使用ImageLoader来进行测试列表图片异步加载以及缓存 * 项目名:FastDev4Android * 包名:com.chinaztt.fda.test * 作者:江清清 on 15/11/12 15:19 * 邮箱:jiangqqlmj@163.com * QQ: 781931404 * 公司:江苏中天科技软件技术有限公司 */ @EActivity(R.layout.base_adapter_test_layout) public class VolleyLoaderActivity extends BaseActivity { @ViewById ListView lv_base_adapter; @ViewById TextView tv_title; private QuickAdapter<ModuleBean>mAdapter; private List<ModuleBean> moduleBeans; private RequestQueue requestQueue; private ImageLoader imageLoader; private ImageLoader.ImageListener listener; @Override protected void onCreate(BundlesavedInstanceState) { super.onCreate(savedInstanceState); requestQueue=Volley.newRequestQueue(this); imageLoader=new ImageLoader(requestQueue,new Fdv_ImageCache()); } @AfterViews public void setViews(){ tv_title.setText("Loader 列表图片"); } @AfterViews public void bindLvData(){ moduleBeans=DataUtils.getAdapterData(); if(mAdapter==null) { mAdapter = new QuickAdapter<ModuleBean>(this, R.layout.lv_item_base_layout,moduleBeans){ @Override protected void convert(BaseAdapterHelper helper, ModuleBean item) { //列表底下显示进度 mAdapter.showIndeterminateProgress(true); helper.setText(R.id.text_lv_item_title, item.getModulename()) .setText(R.id.text_lv_item_description, item.getDescription()); //setImageUrl(R.id.img_lv_item, item.getImgurl()); //使用ImageLoader进行加载图片 ImageView loader_img=helper.getView(R.id.img_lv_item); listener=ImageLoader.getImageListener(loader_img,R.drawable.ic_loading,R.drawable.ic_loading); imageLoader.get(item.getImgurl(),listener); } }; lv_base_adapter.setAdapter(mAdapter); } } } ~~~ 运行效果如下: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569c8eba11825.jpg) ## (四).GsonRequest封装 前面我们已经使用过JsonObjectRequest,JsonArrayRequest两个工具类,这边是采用Android自身提供的JSONObject和JSONArray来进行实现解析JSON数据的,不过我们也知道如果我们已经有JSONObject和JSONArray对象然后一步步的解析还是挺麻烦的,所以我们这边可以使用JSON快速解析框架Gson来进行解决这个问题。现在我们自定义一个GsonRequest来专门处理请求GSON解析。在做之前我们先要看一下StringRequest的实现,首先StringRequest是继承Request类然后实现两个抽象方法,间接着提供若干个构造方法(进行数据初始化,请求类型GET/POST…,请求服务器地址,相应成功是否回调接口)。因为StringRequest的实现代码很少而且较简单,那么我们模仿StringRequest类的实现可以写出来一下GsonRequest代码如下: ~~~ /** * 当前类注释:进行扩展GSON数据解析json数据 * 项目名:FastDev4Android * 包名:com.android.volley.toolbox * 作者:江清清 on 15/11/12 18:28 * 邮箱:jiangqqlmj@163.com * QQ: 781931404 * 公司:江苏中天科技软件技术有限公司 */ public class GsonRequest<T> extends Request<T> { private final Response.Listener<T>listener; private Gson gson; private Class<T> mClass; /** * GsonRequest 构造函数 * @param method 请求方法 * @param url 请求地址 * @param listener 数据请求成功回调接口 * @param errorListener 数据请求失败回调接口 * @param pClass 需要进行解析的类 */ public GsonRequest(int method,Stringurl,Response.Listener<T> listener,Response.ErrorListenererrorListener,Class<T> pClass){ super(method,url,errorListener); this.listener=listener; gson=new Gson(); mClass=pClass; } /** * GsonRequest 构造函数 默认使用GET请求方法 * @param url * @param listener * @param errorListener * @param pClass */ public GsonRequest(Stringurl,Response.Listener<T> listener,Response.ErrorListenererrorListener,Class<T> pClass){ super(Method.GET,url,errorListener); this.listener=listener; gson=new Gson(); mClass=pClass; } /** * 数据解析 * @param response Response from thenetwork 网络请求返回数据 * @return */ @Override protected Response<T> parseNetworkResponse(NetworkResponse response) { try { String jsonStr=new String(response.data,HttpHeaderParser.parseCharset(response.headers)); Tdata=gson.fromJson(jsonStr,mClass); returnResponse.success(data,HttpHeaderParser.parseCacheHeaders(response)); } catch (UnsupportedEncodingExceptione) { return Response.error(newParseError(e)); } } /** * 数据分发 * @param response The parsed responsereturned by */ @Override protected void deliverResponse(T response){ listener.onResponse(response); } } ~~~ 以上实现详解:GsonRequest继承Request,同样提供两个构造函数,然后在parseNetworkResponse()方法中进行解析数据,最后通过Gson组件来封装数据成Bean对象,最终数据回调即可。具体使用方法如下: ~~~ GsonRequest<UpdateBean> gsonRequest=new GsonRequest<UpdateBean>("http://interface.zttmall.com/update/mallUpdate",new Response.Listener<UpdateBean>() { @Override public void onResponse(UpdateBean response) { tv_result.setVisibility(View.VISIBLE); img_result.setVisibility(View.GONE); tv_result.setText(response.toString()); } }, new Response.ErrorListener(){ @Override public void onErrorResponse(VolleyError error) { } }, UpdateBean.class); ~~~ 运行效果如下: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569c8eba8fdd4.jpg) ## (五).Volley二次封装(StringRequest为例) 我们在使用Volley框架请求网络数据获取的时候,一般就是以下三步骤: 1.创建RequestQueue对象 2.创建XXRequest对象(XX代表String,JSON,Image等等) 3.把XXRequest对象添加到RequestQueue中即可 虽然代码量不是不多,不过也还是需要做创建队列对象和请求对象加入到队列中的操作,而创建XxxRequest对象的构造函数中的参数又比较多,所以这边想了一种方案就是在进行网络请求的时候创建和add操作放在内部然后构造函数中的参数减少。所以这边只是以StringRequest为例,简单的封装了一个Get方法请求的方法,其他的功能还有待继续添加更新。 我们已StringRequest为例二次封装了一个类Fdv_StringRequst,我们来看以下该类的使用方法代码: ~~~ newFdv_StringRequest<String>(VolleyTestActivity.this).get("http://www.baidu.com",new Fdv_CallBackListener<String>() { @Override public void onSuccessResponse(String response) { } @Override public void onErrorResponse(VolleyError error) { } }); ~~~ 这样即可,不在需要以前的RequestQueuerequestQueue=Volley.newRequestQueue(this);和 requestQueue.add(object)这两句代码了,直接在内部已经做了。然后如果Get请求直接调用get()方法即可,不需要显示传入请求方法。下面我们来看一下具体实现: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569c8ebaba04e.jpg) 5.1.定义Fdv_Volley类,里边是一个单例方法,来获取RequestQueue请求队列对象. ~~~ /** * 当前类注释:全局Fdv_Volley封装类管理类 * 项目名:FastDev4Android * 包名:com.chinaztt.fdv * 作者:江清清 on 15/11/11 23:02 * 邮箱:jiangqqlmj@163.com * QQ: 781931404 * 公司:江苏中天科技软件技术有限公司 */ public class Fdv_Volley { private static RequestQueue instance; public static RequestQueue getInstace(Context pContext){ if(instance==null){ instance=Volley.newRequestQueue(pContext); } return instance; } } ~~~ 5.2.定义XxxRequest的基类Fdv_BaseRequest,主要功能获取请求队列对象,然后定义一个addRequest()方法,让子类进行调用,添加当前请求对象到请求队列中。 ~~~ /** * 当前类注释: * 项目名:FastDev4Android * 包名:com.chinaztt.fdv * 作者:江清清 on 15/11/11 22:59 * 邮箱:jiangqqlmj@163.com * QQ: 781931404 * 公司:江苏中天科技软件技术有限公司 */ public class Fdv_BaseRequest{ protected static RequestQueue requestQueue; private Context mContext; protected Fdv_BaseRequest(ContextpContext){ this.mContext=pContext; } /** * 请求加入到Volley Request请求队列中 * @param request */ protected void addRequest(Request request){ Fdv_Volley.getInstace(mContext).add(request); } ~~~ 5.3.请求结果回调接口Fdv_CallBackListener,这边暂时只加了两个很简单的方法,后期会进行扩展 ~~~ /** * 当前类注释: * 项目名:FastDev4Android * 包名:com.chinaztt.fdv * 作者:江清清 on 15/11/11 23:18 * 邮箱:jiangqqlmj@163.com * QQ: 781931404 * 公司:江苏中天科技软件技术有限公司 */ public interface Fdv_CallBackListener<T> { void onSuccessResponse(T response); void onErrorResponse(VolleyError error); } ~~~ 5.4.最后是我们的核心类封装过后的StringRequest,Fdv_StringRequest该类继承Fdv_BaseRequest类,在该类中我们暂时只是提供一个get()方法来获取数据中,get()方法的实现还是使用原来的StringRequest进行获取数据,得到数据使用接口回调即可,实现代码如下: ~~~ /** * 当前类注释:Volley 字符串、文本数据请求封装类 * 项目名:FastDev4Android * 包名:com.chinaztt.fdv * 作者:江清清 on 15/11/11 13:43 * 邮箱:jiangqqlmj@163.com * QQ: 781931404 * 公司:江苏中天科技软件技术有限公司 */ public class Fdv_StringRequest<T> extends Fdv_BaseRequest{ public Fdv_StringRequest(Context pContext){ super(pContext); } public void get(String url, finalFdv_CallBackListener<T> listener){ StringRequest stringRequest=new StringRequest(Request.Method.GET, url, new Response.Listener<String>() { @Override public void onResponse(Stringresponse) { if(listener!=null){ listener.onSuccessResponse((T) response); } } }, new Response.ErrorListener() { @Override public voidonErrorResponse(VolleyError error) { if(listener!=null){ listener.onErrorResponse(error); } } }); addRequest(stringRequest); } } ~~~ OK有了以上的几步骤,我们的对于StringRequest的get方法请求简单封装就完成了,有了这样的封装我们的功能实现就会变得代码稍微少了一点,当然我们现在的封装的功能还非常的弱,后面我这边会继续更新封装功能以及重构,让整个工具类变得更加使用。具体更新代码都会FastDev4Android项目中。后期二次封装的Volley框架代码会在Github库中更新,敬请期待~ 以上所有的功能测试代码如下: ~~~ @EActivity(R.layout.volley_test_layout) public class VolleyTestActivity extends BaseActivity{ private static final StringTAG=VolleyTestActivity.class.toString(); @ViewById LinearLayout top_bar_linear_back; @ViewById TextView top_bar_title,tv_result; @ViewById ImageView img_result; @ViewById Buttonbtn_string,btn_json,btn_image_request,btn_image_loader,btn_image_network,btn_string_post,btn_loader_list,btn_gson; @ViewById NetworkImageView img_result_network; private RequestQueue requestQueue; @Override protected void onCreate(BundlesavedInstanceState) { super.onCreate(savedInstanceState); requestQueue=Volley.newRequestQueue(this); } @Click({R.id.top_bar_linear_back,R.id.btn_string,R.id.btn_json,R.id.btn_image_request,R.id.btn_image_loader,R.id.btn_image_network,R.id.btn_string_post,R.id.btn_loader_list,R.id.btn_gson}) public void backLinearClick(View view){ switch (view.getId()){ case R.id.top_bar_linear_back: this.finish(); break; case R.id.btn_string: //获取字符串 Log.d(TAG,"点击获取字符串..."); new Fdv_StringRequest<String>(VolleyTestActivity.this).get("http://www.baidu.com",new Fdv_CallBackListener<String>() { @Override public void onSuccessResponse(String response) { tv_result.setVisibility(View.VISIBLE); img_result.setVisibility(View.GONE); tv_result.setText(response.toString()); } @Override public void onErrorResponse(VolleyError error) { } }); // StringRequest stringRequest=new StringRequest(Request.Method.GET, "http://www.baidu.com",new Response.Listener<String>() { // @Override // public void onResponse(String response) { // tv_result.setVisibility(View.VISIBLE); // img_result.setVisibility(View.GONE); // tv_result.setText(response.toString()); // } // }, new Response.ErrorListener(){ // @Override // public void onErrorResponse(VolleyError error) { // // } // }); // requestQueue.add(stringRequest); break; case R.id.btn_json: //获取json Log.d(TAG,"点击获取json..."); JsonObjectRequest jsonObjectRequest=new JsonObjectRequest(Request.Method.GET , "http://interface.zttmall.com/update/mallUpdate",null,new Response.Listener<JSONObject>() { @Override public void onResponse(JSONObject response) { Gson gson=new Gson(); tv_result.setVisibility(View.VISIBLE); img_result.setVisibility(View.GONE); tv_result.setText(gson.fromJson(response.toString(),UpdateBean.class).toString()); } }, new Response.ErrorListener(){ @Override public void onErrorResponse(VolleyError error) { } }); requestQueue.add(jsonObjectRequest); break; case R.id.btn_image_request: //获取图片 //http:\/\/interface.zttmall.com\/Images\/upload\/image\/20150325\/20150325083110_0898.jpg Log.d(TAG,"点击获取图片..."); ImageRequest imageRequest=new ImageRequest("http://interface.zttmall.com/Images/upload/image/20150325/20150325083110_0898.jpg" , newResponse.Listener<Bitmap>() { @Override public void onResponse(Bitmap response) { tv_result.setVisibility(View.GONE); img_result.setVisibility(View.VISIBLE); img_result.setImageBitmap(response); } }, 0, 0,ImageView.ScaleType.FIT_XY, Bitmap.Config.ARGB_8888, newResponse.ErrorListener() { @Override public void onErrorResponse(VolleyError error) { } }); requestQueue.add(imageRequest); break; case R.id.btn_image_loader: //使用imageloader进行获取图片 ImageLoader imageLoader=new ImageLoader(requestQueue, new Fdv_ImageCache()); tv_result.setVisibility(View.GONE); img_result.setVisibility(View.VISIBLE); ImageLoader.ImageListener listener=ImageLoader.getImageListener(img_result,R.drawable.ic_loading,R.drawable.ic_loading); imageLoader.get("http://interface.zttmall.com//Images//upload//image//20150328//20150328105404_2392.jpg",listener); break; case R.id.btn_image_network: //采用NetworkImageView imageview控件 ImageLoader network_imageLoader=new ImageLoader(requestQueue, new Fdv_ImageCache()); img_result_network.setVisibility(View.VISIBLE); img_result_network.setImageUrl("http://interface.zttmall.com//Images//upload//image//20150325//20150325083214_8280.jpg",network_imageLoader); break; case R.id.btn_string_post: //修改Volley源代码,扩展StringRequest支持post参数设置 Map<String,String>params=new HashMap<String,String>(); params.put("username","zhangsan"); params.put("password","12345"); StringRequestpost_stringRequest=new StringRequest("http://10.18.3.123:8080/SalesWebTest/TestVolleyPost",new Response.Listener<String>() { @Override public void onResponse(String response) { tv_result.setVisibility(View.VISIBLE); img_result.setVisibility(View.GONE); tv_result.setText(response.toString()); } }, new Response.ErrorListener(){ @Override public void onErrorResponse(VolleyError error) { } },params); requestQueue.add(post_stringRequest); break; case R.id.btn_loader_list: //进行使用ImageLoader加载图片列表 openActivity(VolleyLoaderActivity_.class); break; case R.id.btn_gson: //使用扩展工具 GsonRequest进行请求 GsonRequest<UpdateBean> gsonRequest=new GsonRequest<UpdateBean>("http://interface.zttmall.com/update/mallUpdate",new Response.Listener<UpdateBean>() { @Override public void onResponse(UpdateBean response) { tv_result.setVisibility(View.VISIBLE); img_result.setVisibility(View.GONE); tv_result.setText(response.toString()); } }, new Response.ErrorListener(){ @Override public void onErrorResponse(VolleyError error) { } }, UpdateBean.class); requestQueue.add(gsonRequest); break; } } @AfterViews public void setViews(){ top_bar_title.setText("Volley网络框架测试实例"); } } ~~~ 整个功能测试功能点如下图: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569c8ebace62a.jpg) ## (六).结束语: 到此为止我们已经讲完了Volley框架进阶部分:POST请求参数修改,图片缓存器,使用最佳实践,二次封装等相关高级进阶。具体实例和框架注释过的全部代码已经上传到FastDev4Android项目中了。同时欢迎大家去Github站点进行clone或者下载浏览: [https://github.com/jiangqqlmj/FastDev4Android](https://github.com/jiangqqlmj/FastDev4Android) 同时欢迎大家star和fork整个开源快速开发框架项目~ 最后对于Volley框架二次封装的代码库,我这边会继续进行更新,后面会放入单独的Git库中,库地址后面会放出来的,敬请期待~
';

Volley完全解析之基础使用(二十六)

最后更新于:2022-04-01 07:13:17

## (一).前言: 对于网络框架这部分,其实也一直想写点东西总结一下,很长的一段时间对于网络请求这块,在我刚开始做Android开发的时候还是2.2,2.3版本居多,那时候我们的项目中经常使用的是HttpURLConnection和HTTPClient来进行HTTP通信。不过这两种方式的用法有很多复杂的地方,我们在实际使用中都要进行封装,一个HTTP请求自定义封装工具类,方法一多一般就是几百行或者几千行代码,这样如果封装不好就会出现很多重复的代码甚至Bug。慢慢的就出现了很多网络请求框架例如:AsyncHttpClient,OkHttp,google亲儿子Okhttp之类的。现在比较热门的网络请求框架:Volley,Okhttp,这些已经在项目中得到很多的应用。   从我这边的角度来讲,我这边项目网络请求这块的演变如下:HttpURLConnection,HTTPClient->Volley->Okhttp。所以我这边会把Volley和Okhttp这两个框架的使用,进阶,分装都会讲一遍,也方便初学者或者各位能够学会使用以及明白相关的原理。 Volley系列教程大纲如下: 1. Volley完全解析之基础 2. Volley完全解析之进阶 3. Volley完全解析之源码分析        今天我们首先来进入Volley使用基础阶段,会涉及到字符串,JSON,图片等等网络数据请求相关方法的使用。 FastDev4Android框架项目地址:[https://github.com/jiangqqlmj/FastDev4Android](https://github.com/jiangqqlmj/FastDev4Android) ## (二).Volley基本介绍: 2.1.Volley简介: 2013年Google  I/O大会的时候GoogleAndroid开发团队推出了一个新网络数据请求框架Volley网络通信库,它能够使得通信速度更加快,更加简单以及更健壮。看一下官网的发布图片: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569c8eb843d6d.jpg) 发射火弓箭的图有点类似流星,从这幅图我们可以看出来Volley比较适合数据量不大但是通信频繁的场景。它当初的设计目标就是非常适合去进行数据量不大,但是网络通信比较频繁的操作。但是如果数据量很大的网络例如:文件下载这类的,Volley框架的效率就会比较差了。 2.2.Volley功能特点: 1. 提供字符串,文本,JSON图片等数据的异步下载,同时提供回调接口 2. Request请求的优先级以及排序 3. 网络请求,图片等相关缓存功能 4. 网络请求cancel机制 5. 和原生组件生命周期联动机制(例如:Activity结束的时候,会取消掉网络请求)等 2.3.Volley框架依赖配置: Volley库地址为:https://android.googlesource.com/platform/frameworks/volley你可以使用git进行克隆一份,当然这个地址是被墙掉得。你也可以去github中进行clone下载;  [https://github.com/mcxiaoke/android-volley](https://github.com/mcxiaoke/android-volley) 然后采用library依赖的方式导入项目,或者编译成jar包,在进行引入。当然如果你使用的是AndroidStudioIDE,你可以直接添加以下依赖即可: ~~~ compile'com.mcxiaoke.volley:library:1.0.19' ~~~          在我们今天演示的例子项目中,我这边直接采用库依赖的方式,如果大家如果clone我这边FastDev4Android项目,可以直接看到源代码,便于后面我们对于源代码的修改以及功能封装。 ## (三).Volley使用讲解: OK有了上面的准备工作,我们现在开始正式使用Volley框架进行我们的网络请求,要实现网络数据请求主要要记住下面三步骤: 1.创建RequestQueue对象 2.创建XXRequest对象(XX代表String,JSON,Image等等) 3.把XXRequest对象添加到RequestQueue中即可 3.1.StringRequest使用    我们现在开始使用StringRequest来获取文本字符串数据,首先创建RequestQueue对象: ~~~ RequestQueue requestQueue=Volley.newRequestQueue(this); ~~~ 该通过Volley的静态方法newRequestQueue(Context)来获取一个请求队列对象,内部设计有请求缓存,系统版本区分请求,并且设计适合于高并发。间接着创建StringRequest对象来发起HTTP请求,然后接收相应数据: ~~~ StringRequest stringRequest=new StringRequest(Request.Method.GET, "http://www.sina.com.cn", new Response.Listener<String>() { @Override public void onResponse(String response) { tv_result.setVisibility(View.VISIBLE); img_result.setVisibility(View.GONE); tv_result.setText(response.toString()); } }, new Response.ErrorListener(){ @Override public void onErrorResponse(VolleyError error) { } }); ~~~ 创建StringRequest对象传入四个参数: 1. 请求方法GET/POST/或者其他 2. 请求服务器的目标地址 3. 服务器数据成功响应回调 4. 服务器数据失败响应回调 第三步,就是StringRequest请求对象加入请求队列中(RequestQueue) ~~~ requestQueue.add(jsonObjectRequest); ~~~ 以上就完成了StringRequest的请求操作,运行效果如下图: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569c8eb88c106.jpg) 上面我们就是完成了StringRequest的请求一种实例,不过上面只是简单的Get请求,Volley框架也还是支持Post请求,但是比较遗憾的是Volley框架没有直接提供设置Post请求参数的方法,但是我们实现getParams()方法来添加参数,更加让我们开发使用起来简单的方法就是修改Volley框架源代码,后面使用进阶中我们会将到源代码相关修改。现在我们继续往下看。 3.2.JsonObjectRequest和JsonArrayRequest使用    JsonObjectRequest和JsonArrayRequest都是集成自JsonRequest,而JsonRequest又是Request的子类。所以我们使用起来还是和StringRequest的方式差不多。 ~~~ JsonObjectRequest jsonObjectRequest=new JsonObjectRequest(Request.Method.GET , "http://interface.zttmall.com/update/mallUpdate",null,new Response.Listener<JSONObject>() { @Override public void onResponse(JSONObject response) { Gson gson=new Gson(); tv_result.setVisibility(View.VISIBLE); img_result.setVisibility(View.GONE); tv_result.setText(gson.fromJson(response.toString(),UpdateBean.class).toString()); } }, new Response.ErrorListener(){ @Override public void onErrorResponse(VolleyError error) { } }); requestQueue.add(jsonObjectRequest); ~~~ 这边例子是请求一个地址,返回的是JSON数据格式的数据,我们这边使用还定义了一个UpdateBean实体类,使用Gson来对数据进行解析,最后把对象打印出来。效果如下: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569c8eb8f06d1.jpg) 查看以上的onResponse(JSONObject object)回调方法,传入参数为JSONObject对象,因为我们的接口返回就是单条的JSON数据,内部已经把JSON数据格式化成JSONObject对象了。得到对象我们直接解析即可。至于JsonArrayRequest的使用和JsonRequest使用差不多,一个单条,一个是多条而已。这边就不演示了。 3.3.ImageRequest使用 学完上面的文本,josn等数据的加载,现在我们来看一下图片的加载模块了,Volley对于图片加载也给我们提供相应的工具类以及相关方法,主要是以下三种: 1. ImageRequest 2. ImageLoder 3. NetWorkImageView 下面我们来分别学习一下这三种图片加载方法,首先讲一下ImageRequest ImageRequest也是继承自Request类,用法步骤和上面的三步骤战略差不多,整体代码如下: ~~~ ImageRequest imageRequest=new ImageRequest("http://interface.zttmall.com/Images/upload/image/20150325/20150325083110_0898.jpg" , newResponse.Listener<Bitmap>() { @Override public void onResponse(Bitmap response) { tv_result.setVisibility(View.GONE); img_result.setVisibility(View.VISIBLE); img_result.setImageBitmap(response); } }, 300, 300,ImageView.ScaleType.FIT_XY, Bitmap.Config.ARGB_8888, newResponse.ErrorListener() { @Override public void onErrorResponse(VolleyError error) { } }); requestQueue.add(imageRequest); ~~~ 创建ImageRequest对象,然后加入到请求队列中即可,ImageRequest创建的时候需要传入一下参数: * 图片地址 * 图片请求成功回调 * 图片设置宽 * 图片设置高 * 图片拉伸类型 * 图片属性 * 图片请求失败回调 最后ImageRequest对象添加到请求队列中即可。然后加载显示图片如下: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569c8eb9231f9.jpg) 3.4.ImageLoader使用 现在我们来看图片加载第二种方式使用ImageLoader,该类的使用方法就需要改变一下,因为ImageLoader可以把图片缓存结合进去,例如我们集合LruCache工具。所以整体来讲还是比ImageRequest功能强大了一点。使用步骤如下: 1. 创建RequestQueue请求队列对象 2. 创建ImageLoader图片加载器对象 3. 获取ImageListener图片监听器 4. 带哦用ImageLoader.get()方法来进行加载图片 下面我们来具体演示一下使用ImageLoder来加载图片。 首先创建RequestQueue对象: ~~~ RequestQueue requestQueue=Volley.newRequestQueue(this); ~~~ 创建ImageLoader对象,需要创建图片缓存器 ~~~ packagecom.chinaztt.fdv; importandroid.graphics.Bitmap; importcom.android.volley.toolbox.ImageLoader; /** * 当前类注释:图片缓存器,实现ImageLoder.ImageCache实现其中的方法,具体图片怎么样缓存让我们自己来实现 * 这样可以考虑到将来的扩展性 * 项目名:FastDev4Android * 包名:com.chinaztt.fdv * 作者:江清清 on 15/11/12 12:31 * 邮箱:jiangqqlmj@163.com * QQ: 781931404 * 公司:江苏中天科技软件技术有限公司 */ public class Fdv_ImageCache implements ImageLoader.ImageCache { @Override public Bitmap getBitmap(String url) { return null; } @Override public void putBitmap(String url, Bitmapbitmap) { } } ~~~ 这边我创建了一个Fdv_ImagCache类实现ImageLoader.ImageCache接口,并且实现其中的两个方法,这样图片的缓存具体方式让我们用户自己来实现,可以增强系统缓存的可扩展性,自定义搞定。这边我们暂时不具体实现方法直接给了空实现,后面的进阶讲解中会具体来将这些问题。下面为ImageLoader对象创建: ~~~ ImageLoader imageLoader=new ImageLoader(requestQueue, new Fdv_ImageCache()); ~~~ 分别传入请求队列,以及图片缓存器即可。 下边获取ImageListener监听器: ~~~ ImageLoader.ImageListener listener=ImageLoader.getImageListener(img_result,R.drawable.ic_loading,R.drawable.ic_loading); ~~~ 直接通过ImageLoader的静态方法getImageListener()来进行获取,传入三个参数分别为: * 需要实现图片的控件imageview * 正在加载过程中的默认图片ID * 图片下载失败显示的图片ID 最后使用ImageLoader调用get方法来下载并且显示图片 ~~~ imageLoader.get("http://interface.zttmall.com//Images//upload//image//20150328//20150328105404_2392.jpg",listener); ~~~ OK这样就搞定了,让我们来看效果显示: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569c8eb955808.jpg) 3.5.NetworkImageView使用 最后我们来讲一下第三种方式加载图,NetworkImageView,该为自定义的ImageView控件,对ImageView的功能进行了扩展,加入了图片下载的功能,具体使用方法如下: 1. 创建RequestQueue对象 2. 创建Imageloader对象 3. 布局文件使用NetworkImageView自定义控件 4. 使用获取控件调用setImageUrl()方法即可 布局文件如下: ~~~ <com.android.volley.toolbox.NetworkImageView android:id="@+id/img_result_network" android:layout_width="300dp" android:layout_height="300dp" android:layout_centerHorizontal="true"/> ~~~ 间接着创建ImageLoader对象: ~~~ ImageLoader network_imageLoader=new ImageLoader(requestQueue, new Fdv_ImageCache()); ~~~ 然后获取NetworkImageView控件并且调用setimageUrl()方法如下: ~~~ img_result_network.setImageUrl("http://interface.zttmall.com//Images//upload//image//20150325//20150325083214_8280.jpg",network_imageLoader); ~~~         setImageUrl()传入图片地址以及图片加载器imageloader,这样就可以自动下载图片并且进行显示,不过在此之前,我们可以对该控件进行相关设置:setDefaultImageResId(),  setErrorImageResId()分别设置默认加载图片或者图片下载失败显示图片。 具体实例效果如下: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569c8eb9b49ba.jpg) ## (四).结束语: 到此为止我们已经讲完了Volley框架的基本介绍以及相关工具的使用(例如:StringRequest,JsonRequest,JsonArrayRequest,ImageRequest,ImageLoader,NetworkImageView等),具体例如Post请求参数修改,图片缓存器,使用最佳实践等相关高级进阶相关会在下一讲中更新。具体实例和框架注释过的全部代码已经上传到FastDev4Android项目中了。 同时欢迎大家去Github站点进行clone或者下载浏览: [https://github.com/jiangqqlmj/FastDev4Android](https://github.com/jiangqqlmj/FastDev4Android) 同时欢迎大家star和fork整个开源快速开发框架项目~
';

BaseAdapterHelper详解源码分析,让你摆脱狂写一堆Adapter烦恼(二十五)

最后更新于:2022-04-01 07:13:15

## (一).前言: Base-Adater-Helper是对我们传统的BaseAdapter的ViewHolder的模式的一个抽象封装,主要的功能可以让我们简化的书写AbsListView,例如ListView,GridView的自定义Adapter的代码,上一篇我们已经对该项目的基本使用做了介绍实例,今天我们来对该项目的实现详解源码分析一下,同时我们可以对此框架进行扩展开发。 FastDev4Android框架项目地址:[https://github.com/jiangqqlmj/FastDev4Android](https://github.com/jiangqqlmj/FastDev4Android) 基本使用方式如下: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569c8eb7ebc67.jpg) 我们看一下它的实例使用方法: ~~~ mAdapter = newQuickAdapter<ModuleBean>(this, R.layout.lv_item_base_layout,moduleBeans){ @Override protected voidconvert(BaseAdapterHelper helper, ModuleBean item) { //列表底下显示进度 mAdapter.showIndeterminateProgress(true); helper.setText(R.id.text_lv_item_title, item.getModulename()) .setText(R.id.text_lv_item_description, item.getDescription()) .setImageUrl(R.id.img_lv_item, item.getImgurl()); } }; lv_base_adapter.setAdapter(mAdapter); ~~~ ## (二).总体分析: 整个项目其实比较简单也就是四个主要的类: * BaseAdapterHelper * BaseQuickAdapter * EnhancedQuickAdapter * QuickAdapter 通过阅读整个项目源代码之后发现,当前实现也是基于ViewHolder模式的,等会我 们分析就知道了。其中view类型相关的采用泛型存储,最重要的数据绑定工作,采用抽象函数convert()实现,全部交给用户来实现自定义的绑定。 从上面的基本使用中发现,我们在使用该base-adapter-helper过程中,只需要创建newQuickAdapter()传入布局,数据,控件和数据模型绑定即可。但是我们查看QuickAdapter类的代码发现: ~~~ packagecom.chinaztt.fda.adapter.base; importandroid.content.Context; importandroid.view.View; importandroid.view.ViewGroup; importjava.util.List; import staticcom.chinaztt.fda.adapter.base.BaseAdapterHelper.get; /** * Abstraction class of a BaseAdapter in whichyou only need * to provide the convert()implementation.<br/> * Using the provided BaseAdapterHelper, yourcode is minimalist. * @param <T> The type of the items inthe list. */ public abstractclass QuickAdapter<T> extends BaseQuickAdapter<T,BaseAdapterHelper> { /** * Create a QuickAdapter. * @param context The context. * @param layoutResId The layout resourceid of each item. */ public QuickAdapter(Context context, intlayoutResId) { super(context, layoutResId); } /** * Same asQuickAdapter#QuickAdapter(Context,int) but with * some initialization data. * @param context The context. * @param layoutResId The layout resourceid of each item. * @param data A new list is created out of this oneto avoid mutable list */ public QuickAdapter(Context context, intlayoutResId, List<T> data) { super(context, layoutResId, data); } /** * 进行获取类ViewHolder的 BaseAdapterHelper * @param position The position of the item within theadapter's data set of the item whose view we want. * @param convertView The old view toreuse, if possible. Note: You should check that this view * is non-null and of anappropriate type before using. If it is not possible to convert * this view to display thecorrect data, this method can create a new view. * Heterogeneous lists canspecify their number of view types, so that this View is * always of the right type(see {@link #getViewTypeCount()} and * {@link#getItemViewType(int)}). * @param parent The parent that this view will eventuallybe attached to * @return */ protected BaseAdapterHelpergetAdapterHelper(int position, View convertView, ViewGroup parent) { return get(context, convertView,parent, layoutResId, position); } } ~~~ 其实它并没有做什么其他更多的时候,就只有两个构造方法,和一个获取BaseAdapterHelper对象的方法。所以要研究该框架,我们只需要研究它的父类BaseQuickAdapter和BaseAdapterHelper类即可。最后简单的看一下EnhancedQuickAdapter类。 2.1.BaseQuickAdapter类:该类继承了BaseAdapter类,同时实现了BaseAdapter中通用的几个抽象方法(也就是我们平时自定义Adapter需要实现的几个方法),完成了Adapter要做的绝大多数操作以及对于Data操作的方法(不过个人赶脚用处不是特别大哈~个人见解)。该类还有两个泛型数据,其中T代表数据,H针对BaseAdapterHelper。 ~~~ packagecom.chinaztt.fda.adapter.base; importandroid.content.Context; importandroid.view.Gravity; importandroid.view.View; importandroid.view.ViewGroup; importandroid.widget.BaseAdapter; importandroid.widget.FrameLayout; importandroid.widget.ProgressBar; importjava.util.ArrayList; importjava.util.List; /** * Abstraction class of a BaseAdapter in whichyou only need * to provide the convert()implementation.<br/> * Using the provided BaseAdapterHelper, yourcode is minimalist. * @param <T> The type of the items inthe list. */ public abstractclass BaseQuickAdapter<T, H extends BaseAdapterHelper> extendsBaseAdapter { protected static final String TAG =BaseQuickAdapter.class.getSimpleName(); //上下文引用 protected final Context context; //需要显示的布局id protected final int layoutResId; //需要显示的数据 protected final List<T> data; //是否显示进度 protected booleandisplayIndeterminateProgress = false; /** * Create a QuickAdapter. * @param context The context. * @param layoutResId The layout resourceid of each item. */ public BaseQuickAdapter(Context context,int layoutResId) { this(context, layoutResId, null); } /** * Same asQuickAdapter#QuickAdapter(Context,int) but with * some initialization data. * @param context The context. * @param layoutResId The layout resourceid of each item. * @param data A new list is created out of this oneto avoid mutable list */ public BaseQuickAdapter(Context context,int layoutResId, List<T> data) { this.data = data == null ? newArrayList<T>() : new ArrayList<T>(data); this.context = context; this.layoutResId = layoutResId; } /** * 判断是否需要显示进度,如果显示 数量+1 * @return */ @Override public int getCount() { int extra =displayIndeterminateProgress ? 1 : 0; return data.size() + extra; } /** * 判断索引是否大于等于数据长度,如果等于,最后一个item应该为进度,那么返回的数据对象为null即可 * @param position * @return */ @Override public T getItem(int position) { if (position >= data.size()) returnnull; return data.get(position); } @Override public long getItemId(int position) { return position; } /** * view类型返回2,这边还需要显示进度bar * @return */ @Override public int getViewTypeCount() { return 2; } /** * 进行判断索引是不是已经大于等于数据的长度 * 如果超过或者等于数据的长度 返回1 * @param position * @return */ @Override public int getItemViewType(int position) { return position >= data.size() ? 1 :0; } @Override public View getView(int position, ViewconvertView, ViewGroup parent) { if (getItemViewType(position) == 0) { //获取适配器helper --相当于获取ViewHolder-BaseAdapterHelper final H helper =getAdapterHelper(position, convertView, parent); //获取item model 数据 T item = getItem(position); //给子类QuickAdapter来进行实现,不过QuickAdapter也是抽象类,给具体创建的类进行实现 convert(helper, item); helper.setAssociatedObject(item); return helper.getView(); } //显示进度 returncreateIndeterminateProgressView(convertView, parent); } /** * 创建进度条 显示在view的结尾 * @param convertView * @param parent * @return */ private ViewcreateIndeterminateProgressView(View convertView, ViewGroup parent) { if (convertView == null) { FrameLayout container = newFrameLayout(context); container.setForegroundGravity(Gravity.CENTER); ProgressBar progress = newProgressBar(context); container.addView(progress); convertView = container; } return convertView; } @Override public boolean isEnabled(int position) { return position < data.size(); } //==================================================== //下面的方法基本封装了操作集合相关的 //主要为新增add,设置set,移除remove以及替换,是否存在判断 //===================================================== public void add(T elem) { data.add(elem); notifyDataSetChanged(); } public void addAll(List<T> elem) { data.addAll(elem); notifyDataSetChanged(); } public void set(T oldElem, T newElem) { set(data.indexOf(oldElem), newElem); } public void set(int index, T elem) { data.set(index, elem); notifyDataSetChanged(); } public void remove(T elem) { data.remove(elem); notifyDataSetChanged(); } public void remove(int index) { data.remove(index); notifyDataSetChanged(); } public void replaceAll(List<T> elem){ data.clear(); data.addAll(elem); notifyDataSetChanged(); } public boolean contains(T elem) { return data.contains(elem); } /** * 清空数组 */ /** Clear data list */ public void clear() { data.clear(); notifyDataSetChanged(); } public voidshowIndeterminateProgress(boolean display) { if (display ==displayIndeterminateProgress) return; displayIndeterminateProgress = display; notifyDataSetChanged(); } /** * 实现该方法,让用户自己绑定控件和数据 * Implement this method and use the helperto adapt the view to the given item. * @param helper A fully initializedhelper. * @param item The item that needs to be displayed. */ protected abstract void convert(H helper, Titem); /** * You can override this method to use acustom BaseAdapterHelper in order to fit your needs * @param position The position of the item within theadapter's data set of the item whose view we want. * @param convertView The old view toreuse, if possible. Note: You should check that this view * is non-null and of anappropriate type before using. If it is not possible to convert * this view to display thecorrect data, this method can create a new view. * Heterogeneous lists canspecify their number of view types, so that this View is * always of the right type(see {@link #getViewTypeCount()} and * {@link#getItemViewType(int)}). * @param parent The parent that this view will eventuallybe attached to * @return An instance of BaseAdapterHelper */ protected abstract H getAdapterHelper(intposition, View convertView, ViewGroup parent); } ~~~ 重点我们来看下这个类的相关实现: 2.1.1.成员变量context为获取控件所需要的上下文,layoutResId为布局文件的id。其中data的类型是List的,由于这边传入的实体信息可能是不同类型的集合,所以这边采用了泛型来进行定义了。 2.1.2.getViewTypeCount(),getItemViewType()这边我们可以看源代码第一个函数返回2,第二个函数会进行判断返回position。因为base-adapter-helper已经实现的progressbar进度的显示控制条。 2.1.3.然后是一些实现adapter都要实现的方法例如:getCount,getView,getItem,getItemId之类的方法,还有一些关于数据操作的方法。 2.1.4.重点看一下getView方法: ~~~ public ViewgetView(int position, View convertView, ViewGroup parent) { if (getItemViewType(position) == 0) { //获取适配器helper --相当于获取ViewHolder-BaseAdapterHelper final H helper =getAdapterHelper(position, convertView, parent); //获取item model 数据 T item = getItem(position); //给子类QuickAdapter来进行实现,不过QuickAdapter也是抽象类,给具体创建的类进行实现 convert(helper, item); helper.setAssociatedObject(item); return helper.getView(); } //显示进度 returncreateIndeterminateProgressView(convertView, parent); } ~~~ 重点地方已经注释了,在这个方法中,会首先进行判断getItemViewType(position)的值,如果返回值不等于0的时候,那就是说显示底部的进度,其他的情况会正常加载view。此时通过抽象方法getAdapterHelper()来进行获取一个BaseAdapterHelper。 该抽象方法的实现类是QuickAdapter: ~~~ /** * 进行获取类ViewHolder的 BaseAdapterHelper * @param position The position of the item within theadapter's data set of the item whose view we want. * @param convertView The old view toreuse, if possible. Note: You should check that this view * is non-null and of anappropriate type before using. If it is not possible to convert * this view to display thecorrect data, this method can create a new view. * Heterogeneous lists canspecify their number of view types, so that this View is * always of the right type(see {@link #getViewTypeCount()} and * {@link#getItemViewType(int)}). * @param parent The parent that this view will eventuallybe attached to * @return */ protected BaseAdapterHelpergetAdapterHelper(int position, View convertView, ViewGroup parent) { return get(context, convertView,parent, layoutResId, position); } ~~~ 其中又去调用BaseAdapterHelper的如下方法: ~~~ /** This method ispackage private and should only be used by QuickAdapter. */ /** * 进行获取类ViewHolder的BaseAdapterHelper对象 * @param context 上下文引用 * @param convertView item view * @param parent 父控件view * @param layoutId 布局ID * @param position 索引 * @return */ static BaseAdapterHelper get(Contextcontext, View convertView, ViewGroup parent, int layoutId, int position) { if (convertView == null) { return newBaseAdapterHelper(context, parent, layoutId, position); } // Retrieve the existing helper andupdate its position BaseAdapterHelper existingHelper =(BaseAdapterHelper) convertView.getTag(); existingHelper.position = position; return existingHelper; } ~~~ 该类中的实现方法就是首先判断convertView是否为null,如果为null,进行创建。否则直接从getTag()进行获取,然后返回当前BaseAdapterHelper,到这边相比大家有没有明白BaseAdapterHelper和我们以前接触的ViewHolder相似了呢?对的,这边的BaseAdapterHelper其实就是充当的ViewHolder角色。 上面我们分析了getAdapterHelper的生成方式,下面我们看一下itemdata的获取,这个比较简单直接如下即可: //获取item model数据     ~~~ T item = getItem(position); ~~~ 该类最后我们来看一个最关键的代码 //给子类QuickAdapter来进行实现,不过QuickAdapter也是抽象类,给具体创建的类进行实现 ~~~ convert(helper, item); ~~~            该convert()为抽象方法,convert的参数为BaseAdapterHelper和实体对象item,让我们用户可以自定义自由绑定数据到控件中。绑定成功之后直接通过调用helper.getView()返回即可。 2.2.BaseAdapterHelper类 上面get()方法的时候已经说过BaseAdapterHelper相当于ViewHolder,同时该类还提供一大堆的方法来让我们进行设置view的数据,属性,以及一些事件方法。 我们首先开看构造方法:          ~~~ protectedBaseAdapterHelper(Context context, ViewGroup parent, int layoutId, intposition) { this.context = context; this.position = position; this.views = newSparseArray<View>(); convertView =LayoutInflater.from(context) //根据布局id来加载view .inflate(layoutId, parent,false); convertView.setTag(this);//相当于存放ViewHolder } ~~~ 该构造方法创建一个稀疏数组来存放view对象(用来绑定数据的)。然后根据布局id来进行获取convertView,同时把当前对象加入到convertView得tag中,然后后面我们可以通过convertView.getTag()来进行获取,这样保持BaseAdapterHelper对于convertView的相互持有引用。 除了以上的构造方法以外,还有另外一个进入BaseAdapterHelper的入口: ~~~ /** * This method is the only entry point toget a BaseAdapterHelper. * @param context The current context. * @param convertView The convertView argpassed to the getView() method. * @param parent The parent arg passed to the getView()method. * @return A BaseAdapterHelper instance. */ public static BaseAdapterHelper get(Contextcontext, View convertView, ViewGroup parent, int layoutId) { return get(context, convertView,parent, layoutId, -1); } /** This method is package private andshould only be used by QuickAdapter. */ /** * 进行获取类ViewHolder的BaseAdapterHelper对象 * @param context 上下文引用 * @param convertView item view * @param parent 父控件view * @param layoutId 布局ID * @param position 索引 * @return */ static BaseAdapterHelper get(Contextcontext, View convertView, ViewGroup parent, int layoutId, int position) { if (convertView == null) { return newBaseAdapterHelper(context, parent, layoutId, position); } // Retrieve the existing helper andupdate its position BaseAdapterHelper existingHelper =(BaseAdapterHelper) convertView.getTag(); existingHelper.position = position; return existingHelper; } ~~~ 上面会进行判断convertView是否存在,不存在进行调用构造方法创建,存在直接从convertView.getTag()来获取BaseAdapterHelper对象。 下面我们来另外的比较重要的两个方法,主要是获取view控件,然后同时加入到稀疏数组中: ~~~ public <T extends View> T getView(intviewId) { return retrieveView(viewId); } ~~~ ~~~ /** * 进行从模板view中获取相应的控件 然后放入到view控件集合中 */ protected <T extends View> TretrieveView(int viewId) { //从集合中获取当前viewId的view,如果集合中不存在,那么从view重findById() //获取出来存放到集合中 并且返回 View view = views.get(viewId); if (view == null) { view =convertView.findViewById(viewId); views.put(viewId, view); } return (T) view; } ~~~ 上面方法我们可以看出,每次调用retrieveView方法的时候,都会根据viewId去views集合中查询一下,如果不存在,那么会通过findViewById(viewId)来进行获取,同时把该view加入到集合中,这样下次查询就不需要重复调用findViewById()了。 其他一些工具方法,例如设置字体,文本,图片,颜色,事件啊等等…. ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569c8eb81e568.jpg) 一大堆的方法这边就不贴代码了,具体每个方法的含义已经在项目源代码中注释了,大家有兴趣可以去下载项目然后去阅读一下: FastDev4Android框架项目地址:[https://github.com/jiangqqlmj/FastDev4Android](https://github.com/jiangqqlmj/FastDev4Android) 2.3.EnhancedQuickAdapter类:该类是继承自QuickAdapter,其他多出下面的两个方法: ~~~ /** * 进行绑定控件和数据 数据发生变化 * @param helper A fully initializedhelper. * @param item The item that needs to be displayed. */ @Override protected final voidconvert(BaseAdapterHelper helper, T item) { boolean itemChanged =helper.associatedObject == null || !helper.associatedObject.equals(item); helper.associatedObject = item; convert(helper, item, itemChanged); } /** * 让用户来自定义 * @param helper The helper to use to adapt the view. * @param item The item you should adapt the view to. * @param itemChanged Whether or not thehelper was bound to another object before. */ protected abstract voidconvert(BaseAdapterHelper helper, T item, boolean itemChanged); ~~~ 仔细看convert()方法,多了第三方参数itemChanged,对于data集合发生改变的时候刷新View。 ## (三).改进点: 我们在进行控件,数据模型进行绑定的时候都是采用set方法才完成。但是我们的需求是千变万化的,就拿图片加载来说,框架内部默认是采用Picasso来加载图片的。但是如果我们的项目想要采用Volley,UIL或者Fresco呢?那么我们不得不需要在里边进行扩展相应的方法来实现了。虽然这种方案可以解决问题,但是如果还有其他方案,那么一直在扩展就会变得很庞大了。幸运的时候convert()方法回调回来的BaseAdapterHelper对象helper,该类中有getView()和retrieveView()方法,同时getView()还是public,对外开放的,我们在convert()实现方法中直接通过getView()即可获取view,然后如何显示图片就看我们自己的了。除此之外里边很多设置字体,文本等等很少辅助类,如果需要设置更多的东西,我们需要不断的扩展。 ## (四).结束语: 经过昨天和今天的讲解介绍,相应大家对于base-adapter-helper的基本使用和源码原理分析有一个比较清晰的了解,以后在项目中也可以多多使用这个,一定会提高写代码的效率。其他这个框架学习分析下来,非常核心的东西也还是比较好理解,重点要看思考,分析吧。继续加油。 到此Base-Adapter-Helper的基本介绍和基本使用已经讲完了,具体实例和框架注释过的全部代码已经上传到FastDev4Android项目中了。 同时欢迎大家去Github站点进行clone或者下载浏览: [https://github.com/jiangqqlmj/FastDev4Android](https://github.com/jiangqqlmj/FastDev4Android) 同时欢迎大家star和fork整个开源快速开发框架项目~
';

BaseAdapterHelper的基本使用介绍,让你摆脱狂写一堆Adapter烦恼(二十四)

最后更新于:2022-04-01 07:13:12

## (一).前言: 项目开发中对于列表(listview)和表格(girdview)中的数据显示,我们就需要写自定义的Adapter。所以一般一个项目下来少得十几个Adapter,多的二十几个甚至更多。但是Adapter的处理一般就是传入数据,view模板,getView,之类的在加入一些控制显示之类的代码。虽然写起来难度不大,但是很多类似的代码经常需要狂写,简直会有想吐的赶脚吧~。今天为大家介绍一款快速Adapter的框架,只需要创建QuickAdapter,传入布局,数据,然后绑定控件以及显示的数据即可。这样一讲是不是赶脚非常方便了,OK我们赶紧行动吧~。 Base-Adapter-Helper[(Github地址)](https://github.com/JoanZapata/base-adapter-helper) FastDev4Android框架项目地址:[https://github.com/jiangqqlmj/FastDev4Android](https://github.com/jiangqqlmj/FastDev4Android) ## (二).基本介绍: 2.1:BaseAdapterHelper是BaseAdapter ViewHolder模板的抽象,让我们更快的写出Adapter来绑定数据显示。官网给出的使用实例和实现效果如下: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569c8eb728064.jpg) 下载该项目下来,整个项目一共四个类文件: * BaseAdapterHelper 可以使得BaseAdapter的getView()方法更加简单的使用。摆脱ViewHolder模板的写法 * QuickAdapter  该类可以让我们自定义的BaseAdapter类的代码大量的缩减,让我们把精力集中在业务实现上面。我们只需要做的是绑定控件view和数据model即可。 * EnhancedQuickAdapter  增强的QuickAdapter,主要是用于绑定控件view和数据model * BaseQuickAdapter  QuickAdapter的高层类封装,实现BaseAdapter中抽象方法,同时对于data数据的操作以及其他操作 2.2.AndroidStudio引入Base-Adapter-Helper库,一种方式最简单了直接使用以下方式依赖: ~~~ compile'com.joanzapata.android:base-adapter-helper:1.1.11' ~~~ 我这边采用的是第二种方式:因为该库只有四个类,所以我就把这个类直接复制进入去了,如下: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569c8eb7505dd.jpg) 不过直接这样还行,因为该项目中setImageUrl()方法中图片异步加载采用的是Picasso框架,所以要么你 修改该框架,要么你就可以以下方式引入该框架: ~~~ compile 'com.squareup.picasso:picasso:2.5.2' ~~~          2.3.BaseAdapterHelper中的方法介绍: * 对于任何TextView:setText()调用setText(String) * 对于任何View:setAlpha()调用setAlpha(float) * 对于任何View:setVisiable()调用setVisibility(int) * 对于任何TextView:linkify()调用Linkify.addLinks(view,ALL) * 对于任何TextView:setTypeface()调用setTypeface(Typeface) * 对于任何ProgressBar:setProgress()调用setProgress(int) * 对于任何ProgressBar:setMax()调用setMax(int) * 对于任何RatingBar:setRating()调用setRating(int) * 对于任何ImageView:setImageResource()调用setImageResource(int) * 对于任何ImageView:setImageDrawable()调用setImageDrawable(Drawable) * 对于任何ImageVIew:setImageBitmap()调用setImageBitmap(bitmap) * 使用setImageUrl()会使用Picasso框架来进行下载图片然后现在ImageView中 * 使用seImageBuilder()会使用Picasso的图片请求构建器 * setOnClickListener() * setOnTouchListener() * setOnLongClickListener() * setTag() * setChecked() * setAdapter() 2.4.我们可以使用showIndeterminateProgress(boolean)可以控制列表底下显示进度。如下: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569c8eb763f4a.jpg) ## (三).使用实例: 有了以上对于框架的基本介绍和引入之后,现在我们可以来具体实现一个例子了,其实用法很简单: * 准备数据(和以往一样) * 创建适配器(创建适配器并且绑定数据和相关布局控件,model即可)--这边就不需要重写一个自定义的适配器了。 * Listview绑定适配器(和以往一样) 3.1.准备数据:ModuleBean和DataUtil ~~~ packagecom.chinaztt.fda.entity; /** *当前类注释:列表数据测试数据实体类 *项目名:FastDev4Android *包名:com.chinaztt.fda.entity *作者:江清清 on 15/11/8 17:56 *邮箱:jiangqqlmj@163.com *QQ: 781931404 * 公司:江苏中天科技软件技术有限公司 */ publicclass ModuleBean { private String modulename; private String imgurl; private String description; public ModuleBean() { } public ModuleBean(String modulename, Stringimgurl, String description) { this.modulename = modulename; this.imgurl = imgurl; this.description = description; } public String getModulename() { return modulename; } public void setModulename(Stringmodulename) { this.modulename = modulename; } public String getImgurl() { return imgurl; } public void setImgurl(String imgurl) { this.imgurl = imgurl; } public String getDescription() { return description; } public void setDescription(Stringdescription) { this.description = description; } @Override public String toString() { return "ModuleBean{" + "modulename='" +modulename + '\'' + ", imgurl='" + imgurl+ '\'' + ", description='" +description + '\'' + '}'; } } ~~~ ~~~ packagecom.chinaztt.fda.test; importcom.chinaztt.fda.entity.ModuleBean; importjava.util.ArrayList; importjava.util.List; /** *当前类注释: *项目名:FastDev4Android *包名:com.chinaztt.fda.test *作者:江清清 on 15/11/8 17:53 *邮箱:jiangqqlmj@163.com *QQ: 781931404 * 公司:江苏中天科技软件技术有限公司 */ publicclass DataUtils { /** * 进行构造相关数据 * @return */ public static List<ModuleBean>getAdapterData(){ List<ModuleBean> moduleBeans=newArrayList<ModuleBean>(); ModuleBean moduleBean=new ModuleBean(); moduleBean.setModulename("完美“价”给你"); moduleBean.setImgurl("http://interface.zttmall.com/Images/upload/image/20150325/20150325083110_0898.jpg"); moduleBean.setDescription("标题1的简要说明"); moduleBeans.add(moduleBean); moduleBean=new ModuleBean(); moduleBean.setModulename("探路者,旅行记"); moduleBean.setImgurl("http://interface.zttmall.com/Images/upload/image/20150325/20150325083214_8280.jpg"); moduleBean.setDescription("标题2的简要说明"); moduleBeans.add(moduleBean); moduleBean=new ModuleBean(); moduleBean.setModulename("进口商品,彻底放价"); moduleBean.setImgurl("http://interface.zttmall.com/Images/upload/image/20150328/20150328105404_2392.jpg"); moduleBean.setDescription("标题3的简要说明"); moduleBeans.add(moduleBean); moduleBean=new ModuleBean(); moduleBean.setModulename("鲜果季"); moduleBean.setImgurl("http://interface.zttmall.com/Images/upload/image/20150325/20150325083611_0644.jpg"); moduleBean.setDescription("标题4的简要说明"); moduleBeans.add(moduleBean); moduleBean=new ModuleBean(); moduleBean.setModulename("盼盼 法式小面包"); moduleBean.setImgurl("http://interface.zttmall.com/Images/upload/image/20150312/20150312100454_8837.jpg"); moduleBean.setDescription("标题5的简要说明"); moduleBeans.add(moduleBean); moduleBean=new ModuleBean(); moduleBean.setModulename("雀巢 脆脆鲨 威化 480g/盒"); moduleBean.setImgurl("http://interface.zttmall.com/Images/upload/image/20150312/20150312100617_0693.jpg"); moduleBean.setDescription("标题6的简要说明"); moduleBeans.add(moduleBean); moduleBean=new ModuleBean(); moduleBean.setModulename("主题馆1"); moduleBean.setImgurl("http://interface.zttmall.com/Images/upload/image/20150129/20150129163540_6179.jpg"); moduleBean.setDescription("标题7的简要说明"); moduleBeans.add(moduleBean); moduleBean=new ModuleBean(); moduleBean.setModulename("主题馆2"); moduleBean.setImgurl("http://interface.zttmall.com/Images/upload/image/20150129/20150129163615_1774.jpg"); moduleBean.setDescription("标题8的简要说明"); moduleBeans.add(moduleBean); moduleBean=new ModuleBean(); moduleBean.setModulename("主题馆3"); moduleBean.setImgurl("http://interface.zttmall.com/Images/upload/image/20150129/20150129163635_1130.jpg"); moduleBean.setDescription("标题9的简要说明"); moduleBeans.add(moduleBean); moduleBean=new ModuleBean(); moduleBean.setModulename("主题馆4"); moduleBean.setImgurl("http://interface.zttmall.com/Images/upload/image/20150129/20150129163840_0270.jpg"); moduleBean.setDescription("标题10的简要说明"); moduleBeans.add(moduleBean); moduleBean=new ModuleBean(); moduleBean.setModulename("主题馆5"); moduleBean.setImgurl("http://interface.zttmall.com/Images/upload/image/20150129/20150129163849_4099.jpg"); moduleBean.setDescription("标题11的简要说明"); moduleBeans.add(moduleBean); return moduleBeans; } } ~~~ 3.2.创建适配器(绑定数据,控件相关)和绑定适配器 ~~~ packagecom.chinaztt.fda.test; importandroid.os.Bundle; importandroid.widget.ListView; importcom.chinaztt.fda.adapter.base.BaseAdapterHelper; importcom.chinaztt.fda.adapter.base.QuickAdapter; importcom.chinaztt.fda.entity.ModuleBean; importcom.chinaztt.fda.ui.R; importcom.chinaztt.fda.ui.base.BaseActivity; importorg.androidannotations.annotations.AfterViews; importorg.androidannotations.annotations.EActivity; importorg.androidannotations.annotations.ViewById; importjava.util.List; /** *当前类注释:baseAdapterhelper 使用实例 *项目名:FastDev4Android *包名:com.chinaztt.fda.test *作者:江清清 on 15/11/8 17:39 *邮箱:jiangqqlmj@163.com *QQ: 781931404 * 公司:江苏中天科技软件技术有限公司 */ @EActivity(R.layout.base_adapter_test_layout) publicclass BaseAdapterTestActivity extends BaseActivity { @ViewById ListView lv_base_adapter; private QuickAdapter<ModuleBean>mAdapter; private List<ModuleBean> moduleBeans; @Override protected void onCreate(BundlesavedInstanceState) { super.onCreate(savedInstanceState); } @AfterViews public void bindLvData(){ moduleBeans=DataUtils.getAdapterData(); if(mAdapter==null) { mAdapter = newQuickAdapter<ModuleBean>(this, R.layout.lv_item_base_layout,moduleBeans){ @Override protected voidconvert(BaseAdapterHelper helper, ModuleBean item) { mAdapter.showIndeterminateProgress(true); helper.setText(R.id.text_lv_item_title, item.getModulename()) .setText(R.id.text_lv_item_description, item.getDescription()) .setImageUrl(R.id.img_lv_item, item.getImgurl()); } }; lv_base_adapter.setAdapter(mAdapter); } } } ~~~ 3.3.运行效果如下 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569c8eb78a076.jpg) 好了到此Base-Adapter-Helper的基本介绍和基本使用已经讲完了,相信大家已经会初步使用了,下一 篇我们对于源代码的实现做一个分析和扩展进阶使用做讲解。具体全部代码已经上传到FastDev4Android项目中了。同时欢迎大家去Github站点进行clone或者下载浏览: [https://github.com/jiangqqlmj/FastDev4Android](https://github.com/jiangqqlmj/FastDev4Android) 同时欢迎大家star和fork整个开源快速开发框架项目~
';

重写WebView网页加载以及JavaScript注入详解(二十三)

最后更新于:2022-04-01 07:13:10

## (一).前言: 今天我们来学习一下重写WebView组件来实现网页的加载,以及我们平时APP开发中经常使用的JS注入,js和java相互调用的问题来重点讲解一下。如果大家都WebView加载还不是太熟悉的话,这边我之前专门写了一个WebView的专题,其他包含基本使用和js注入的问题。[(点击进入WebView进阶专题)](http://blog.csdn.net/column/details/adandroid.html) FastDev4Android框架项目地址:[https://github.com/jiangqqlmj/FastDev4Android](https://github.com/jiangqqlmj/FastDev4Android) ## (二).重写WebView: 2.1首先我们来看一下实现的效果: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569c8eb60a033.jpg)![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569c8eb6a3f09.jpg) 2.2.重写WebView:创建一个HTML5CustomWebView类集成自WebView,然后就是一下几个步骤实现: *  布局文件 *  WebSettings初始化相关设置 *  设置重写WebChromeClient进行相关处理 * 设置重写WebViewClient进行相关处理即可 简单的来说就是以上这几步就可以了 2.3.WebView布局文件主要定义导航栏,网页加载进度,以及WebView容器布局,如下: ~~~ <?xmlversion="1.0" encoding="utf-8"?> <FrameLayoutxmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="fill_parent" android:background="@color/white"> <RelativeLayout android:layout_width="fill_parent" android:layout_height="fill_parent"> <FrameLayout android:id="@+id/fullscreen_custom_content" android:layout_width="fill_parent" android:layout_height="fill_parent" android:background="@color/white" android:visibility="gone"/> <LinearLayout android:layout_width="fill_parent" android:layout_height="fill_parent" android:orientation="vertical" > <!-- 自定义顶部导航功能条 --> <includelayout="@layout/common_top_bar_layout" /> <!-- 中间显示内容 --> <FrameLayout android:id="@+id/main_content" android:layout_width="fill_parent" android:layout_height="fill_parent" android:visibility="gone" /> <!-- 网页加载进度显示 --> <FrameLayout android:id="@+id/frame_progress" android:layout_width="fill_parent" android:layout_height="fill_parent" android:visibility="visible" > <LinearLayout android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" android:orientation="vertical" > <ProgressBar style="@android:style/Widget.ProgressBar.Small" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_horizontal" android:indeterminate="false" android:indeterminateDrawable="@drawable/loading_small" /> <TextView android:id="@+id/webview_tv_progress" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="3dip" android:text="正在加载,已完成0%..." android:textSize="12sp" /> </LinearLayout> </FrameLayout> </LinearLayout> </RelativeLayout> </FrameLayout> ~~~ 2.4.WebSettings初始化设置如下: ~~~ WebSettingswebSettings = this.getSettings(); webSettings.setJavaScriptEnabled(true); //开启javascript webSettings.setDomStorageEnabled(true); //开启DOM webSettings.setDefaultTextEncodingName("utf-8");//设置编码 // // web页面处理 webSettings.setAllowFileAccess(true);// 支持文件流 // webSettings.setSupportZoom(true);// 支持缩放 // webSettings.setBuiltInZoomControls(true);// 支持缩放 webSettings.setUseWideViewPort(true);// 调整到适合webview大小 webSettings.setLoadWithOverviewMode(true);//调整到适合webview大小 webSettings.setDefaultZoom(ZoomDensity.FAR);//屏幕自适应网页,如果没有这个,在低分辨率的手机上显示可能会异常 webSettings.setRenderPriority(RenderPriority.HIGH); //提高网页加载速度,暂时阻塞图片加载,然后网页加载好了,在进行加载图片 webSettings.setBlockNetworkImage(true); //开启缓存机制 webSettings.setAppCacheEnabled(true); //根据当前网页连接状态 if(StrUtils.getAPNType(context)==StrUtils.WIFI){ //设置无缓存 webSettings.setCacheMode(WebSettings.LOAD_DEFAULT); }else{ //设置缓存 webSettings.setCacheMode(WebSettings.LOAD_CACHE_ELSE_NETWORK); } ~~~ 2.5.WebChromeClient进行相关设置处理 ~~~ private classMyWebChromeClient extends WebChromeClient { privateBitmap mDefaultVideoPoster; @Override publicvoid onShowCustomView(View view, CustomViewCallbackcallback) { super.onShowCustomView(view,callback); HTML5CustomWebView.this.setVisibility(View.GONE); if(mCustomView != null) { callback.onCustomViewHidden(); return; } mCustomViewContainer.addView(view); mCustomView= view; mCustomViewCallback= callback; mCustomViewContainer.setVisibility(View.VISIBLE); } @Override publicvoid onHideCustomView() { if(mCustomView == null) { return; } mCustomView.setVisibility(View.GONE); mCustomViewContainer.removeView(mCustomView); mCustomView= null; mCustomViewContainer.setVisibility(View.GONE); mCustomViewCallback.onCustomViewHidden(); HTML5CustomWebView.this.setVisibility(View.VISIBLE); super.onHideCustomView(); } /** * 网页加载标题回调 * @param view * @param title */ @Override publicvoid onReceivedTitle(WebView view, String title) { Log.d("zttjiangqq", "当前网页标题为:" + title); wv_tv_title.setText(title); } /** * 网页加载进度回调 * @param view * @param newProgress */ @Override publicvoid onProgressChanged(WebView view, int newProgress) { // 设置进行进度 ((Activity)mContext).getWindow().setFeatureInt( Window.FEATURE_PROGRESS,newProgress * 100); webview_tv_progress.setText("正在加载,已完成" +newProgress + "%..."); webview_tv_progress.postInvalidate(); //刷新UI Log.d("zttjiangqq", "进度为:" + newProgress); } @Override publicboolean onJsAlert(WebView view, String url, String message, JsResultresult) { returnsuper.onJsAlert(view, url, message, result); } } ~~~ 2.6.WebViewClient进行相关设置处理 ~~~ private classMyWebViewClient extends WebViewClient { /** *加载过程中 拦截加载的地址url * @param view *@param url 被拦截的url * @return */ @Override publicboolean shouldOverrideUrlLoading(WebView view, String url) { Log.i("zttjiangqq","-------->shouldOverrideUrlLoading url:" + url); //这边因为考虑到之前项目的问题,这边拦截的url过滤掉了zttmall://开头的地址 //在其他项目中 大家可以根据实际情况选择不拦截任何地址,或者有选择性拦截 if(!url.startsWith("zttmall://")){ UrimUri = Uri.parse(url); List<String>browerList = new ArrayList<String>(); browerList.add("http"); browerList.add("https"); browerList.add("about"); browerList.add("javascript"); if(browerList.contains(mUri.getScheme())) { returnfalse; }else { Intentintent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); intent.addCategory(Intent.CATEGORY_BROWSABLE); //如果另外的应用程序WebView,我们可以进行重用 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); intent.putExtra(Browser.EXTRA_APPLICATION_ID, FDApplication.getInstance() .getApplicationContext().getPackageName()); try{ FDApplication.getInstance().startActivity(intent); returntrue; }catch (ActivityNotFoundException ex) { } } returnfalse; }else{ returntrue; } } /** * 页面加载过程中,加载资源回调的方法 * @param view * @param url */ @Override publicvoid onLoadResource(WebView view, String url) { super.onLoadResource(view,url); Log.i("zttjiangqq","-------->onLoadResource url:" + url); } /** * 页面加载完成回调的方法 * @param view * @param url */ @Override publicvoid onPageFinished(WebView view, String url) { super.onPageFinished(view,url); Log.i("zttjiangqq","-------->onPageFinished url:" + url); if(isRefresh) { isRefresh= false; } // 加载完成隐藏进度界面,显示WebView内容 frame_progress.setVisibility(View.GONE); mContentView.setVisibility(View.VISIBLE); // 关闭图片加载阻塞 view.getSettings().setBlockNetworkImage(false); } /** * 页面开始加载调用的方法 * @param view * @param url * @param favicon */ @Override publicvoid onPageStarted(WebView view, String url, Bitmap favicon) { Log.d("zttjiangqq","onPageStarted:-----------"+url); super.onPageStarted(view,url, favicon); } @Override publicvoid onReceivedError(WebView view, int errorCode, Stringdescription, String failingUrl) { super.onReceivedError(view,errorCode, description, failingUrl); } @Override publicvoid onScaleChanged(WebView view, float oldScale, float newScale) { super.onScaleChanged(view,oldScale, newScale); HTML5CustomWebView.this.requestFocus(); HTML5CustomWebView.this.requestFocusFromTouch(); } } ~~~ 以上一般使用到得方法已经做了相关的注释。 最后一步就是使用了,使用起来很简单,创建一个Activity,然后把我们定义的WebView加入到布局然后加载网页即可。如下: ~~~ packagecom.chinaztt.fda.html5; importandroid.content.Context; importandroid.content.Intent; importandroid.content.res.Configuration; importandroid.net.Uri; importandroid.os.Bundle; importandroid.view.KeyEvent; importandroid.view.MotionEvent; importandroid.webkit.DownloadListener; importandroid.webkit.JavascriptInterface; /** * 当前类注释: * 项目名:FastDev4Android * 包名:com.chinaztt.fda.html5 * 作者:江清清 on 15/11/06 08:59 * 邮箱:jiangqqlmj@163.com * QQ: 781931404 * 公司:江苏中天科技软件技术有限公司 */ importcom.chinaztt.fda.ui.base.BaseActivity; public classHTML5WebViewCustomAD extends BaseActivity { privateHTML5CustomWebView mWebView; //http://www.zttmall.com/Wapshop/Topic.aspx?TopicId=18 privateString ad_url = "http://www.baidu.com/"; private String title="百度一下你就知道"; @Override publicvoid onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); mWebView= new HTML5CustomWebView(this, HTML5WebViewCustomAD.this,title,ad_url); mWebView.setDownloadListener(newDownloadListener() { @Override publicvoid onDownloadStart(String url, String userAgent, StringcontentDisposition, String mimetype, longcontentLength) { Uriuri = Uri.parse(url); Intentintent = new Intent(Intent.ACTION_VIEW, uri); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); startActivity(intent); } }); //准备javascript注入 mWebView.addJavascriptInterface( newJs2JavaInterface(),"Js2JavaInterface"); if(savedInstanceState != null) { mWebView.restoreState(savedInstanceState); }else { if(ad_url != null) { mWebView.loadUrl(ad_url); } } setContentView(mWebView.getLayout()); } @Override publicvoid onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); if(mWebView != null) { mWebView.saveState(outState); } } @Override protectedvoid onResume() { super.onResume(); if(mWebView != null) { mWebView.onResume(); } } @Override publicvoid onStop() { super.onStop(); if(mWebView != null) { mWebView.stopLoading(); } } @Override protectedvoid onPause() { super.onPause(); if(mWebView != null) { mWebView.onPause(); } } @Override protectedvoid onDestroy() { super.onDestroy(); if(mWebView != null) { mWebView.doDestroy(); } } @Override publicvoid onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); } @Override publicboolean onTouchEvent(MotionEvent event) { returnsuper.onTouchEvent(event); } @Override publicboolean onKeyDown(int keyCode, KeyEvent event) { returnsuper.onKeyDown(keyCode, event); } @Override publicvoid onBackPressed() { if(mWebView != null) { if(mWebView.canGoBack()){ mWebView.goBack(); }else{ mWebView.releaseCustomview(); } } super.onBackPressed(); } /** * JavaScript注入回调 */ publicclass Js2JavaInterface { privateContext context; privateString TAG = "Js2JavaInterface"; @JavascriptInterface publicvoid showProduct(String productId){ if(productId!=null){ //进行跳转商品详情 showToastMsgShort("点击的商品的ID为:" +productId); }else{ showToastMsgShort("商品ID为空!"); } } } } ~~~ ## (三).js注入方法: 仔细查看上面的代码可能大家会发现这样两块地方: ~~~ mWebView.addJavascriptInterface( newJs2JavaInterface(),"Js2JavaInterface"); ~~~ ~~~ public classJs2JavaInterface { privateContext context; privateString TAG = "Js2JavaInterface"; @JavascriptInterface publicvoid showProduct(String productId){ if(productId!=null){ //进行跳转商品详情 showToastMsgShort("点击的商品的ID为:" +productId); }else{ showToastMsgShort("商品ID为空!"); } } } ~~~ 这边就是js注入回调处理的方法,在这边的实例中是使用http://www.zttmall.com/Wapshop/Topic.aspx?TopicId=18这个地址进行加载网页的时候才会生效,因为这边点击网页图片的时候,html代码中加载js方法, 我们来看一下网页的源代码: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569c8eb70484f.jpg) 查看源代码我们就知道 mWebView.addJavascriptInterface( newJs2JavaInterface(),"Js2JavaInterface"); 中第二个参数就是在js方法中调用的对象名字,然后注入对象中回调的方法和js方法中的方法一样即可。这样就完成了一次注入点击回调工作,我们的html就可以和原生java代码发生交互了。使用这种方式非常有助于我们的混合开发。       好了到此重写WebView实现以及js注入的基本使用就讲完了,具体全部代码已经上传到FastDev4Android项目中了。同时欢迎大家去Github站点进行clone或者下载浏览: [https://github.com/jiangqqlmj/FastDev4Android](https://github.com/jiangqqlmj/FastDev4Android) 同时欢迎大家star和fork整个开源快速开发框架项目~
';

列表头生成带文本或者字母的图片开源库TextDrawable使用和详解(二十二)

最后更新于:2022-04-01 07:13:08

## (一).前言: 今天我们主要来学习一下新的开源框架,TextDrawable。我们经常在一些应用通讯录中看到这样的效果,例如我们加了一个好友,但是该用户没有上传头像图片,那么应用要么默认显示一个默认头像,或者我们就可以自动根据姓名第一个字或者首字母自动生成一个图片。OK,TextDrawable框架[(Github地址)](https://github.com/amulyakhare/TextDrawable)就是可以解决这个问题。TextDrawable扩展自Drawable,用于生成文本或者字母的图片的轻量级库,可用于现有/自定义/网络等ImageView类,并且包含一个流接口用于创建drawables以及一个定制的ColorGenerator。效果如下: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569c8eb5222d2.jpg) FastDev4Android框架项目地址:[https://github.com/jiangqqlmj/FastDev4Android](https://github.com/jiangqqlmj/FastDev4Android) ## (二).使用方法: 2.1.AS进行集成配置TextDrawable,使用Gradle进行导入:  ~~~ dependencies { compile'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1' } ~~~ 2.2.创建简单的标题: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569c8eb53aa65.jpg) ~~~ <ImageViewandroid:layout_width="60dp" android:layout_height="60dp" android:id="@+id/image_view"/> ~~~ [注].为ImageView制定了width和height,那么drawable会自动适应大小。 2.3.创建圆角 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569c8eb53aa65.jpg) ~~~ TextDrawabledrawable1 = TextDrawable.builder() .buildRoundRect("A",Color.RED, 10); // radius in px TextDrawabledrawable2 = TextDrawable.builder() .buildRound("A",Color.RED); ~~~ 2.4.增加圆角 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569c8eb5527e9.jpg) ~~~ TextDrawabledrawable = TextDrawable.builder() .beginConfig() .withBorder(4) /* thicknessin px */ .endConfig() .buildRoundRect("A",Color.RED, 10); ~~~ 2.5.修改字体格式 ~~~ TextDrawabledrawable = TextDrawable.builder() .beginConfig() .textColor(Color.BLACK) .useFont(Typeface.DEFAULT) .fontSize(30) /* size in px*/ .bold() .toUpperCase() .endConfig() .buildRect("a",Color.RED) ~~~ 2.6.使用颜色引擎(颜色生成器) ~~~ ColorGeneratorgenerator = ColorGenerator.MATERIAL; // or use DEFAULT // generate randomcolor int color1 =generator.getRandomColor(); // generate colorbased on a key (same key returns the same color), useful for list/grid views int color2 =generator.getColor("user@gmail.com") // declare thebuilder object once. TextDrawable.IBuilderbuilder = TextDrawable.builder() .beginConfig() .withBorder(4) .endConfig() .rect(); // reuse the builderspecs to create multiple drawables TextDrawable ic1 =builder.build("A", color1); TextDrawable ic2 =builder.build("B", color2); ~~~ 2.7.制定宽度和高度 ~~~ <ImageViewandroid:layout_width="wrap_content" android:layout_height="wrap_content" android:id="@+id/image_view"/> ~~~ [注].这边的ImageView采用自使用width/height,可以使用编码来设置drawable的width/heigth。 ~~~ TextDrawabledrawable = TextDrawable.builder() .beginConfig() .width(60) // width in px .height(60) // height in px .endConfig() .buildRect("A",Color.RED); ImageView image =(ImageView) findViewById(R.id.image_view); image.setImageDrawable(drawable); ~~~ 2.8.其他特点 *  可以和其他drawables配合使用,和LayerDrawable,InsetDrawable,AnimationDrawable,TranstionDrawable等等结合使用。 * 和其他视图兼容(不仅仅是ImageView),可以把它作为TextView,Button等等控件的背景图片。 * 使用字母或者其他unicode字符来创建有趣的标题。 ## (三).使用实例: 下面我们来具体实例:首先看一下实现的效果: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569c8eb575d42.jpg) 我们这边创建一个TextDrawablesTestActivity,在里面使用一个列表来分别展示11中不同的效果分别为: 1. SAMPLE_RECT 2. SAMPLE_ROUND_RECT 3. SAMPLE_ROUND 4. SAMPLE_RECT_BORDER 5. SAMPLE_ROUND_RECT_BORDER 6. SAMPLE_ROUND_BORDER 7. SAMPLE_MULTIPLE_LETTERS 8. SAMPLE_FONT 9. SAMPLE_SIZE 10. SAMPLE_ANIMATION 11. SAMPLE_MISC 具体实现代码如下: ~~~ packagecom.chinaztt.fda.test; importandroid.graphics.Color; importandroid.graphics.Typeface; importandroid.graphics.drawable.AnimationDrawable; importandroid.graphics.drawable.Drawable; importandroid.os.Bundle; importandroid.view.LayoutInflater; importandroid.view.View; importandroid.view.ViewGroup; importandroid.widget.BaseAdapter; importandroid.widget.ImageView; importandroid.widget.ListView; importandroid.widget.TextView; importcom.amulyakhare.textdrawable.TextDrawable; importcom.amulyakhare.textdrawable.util.ColorGenerator; importcom.chinaztt.fda.ui.R; importcom.chinaztt.fda.ui.base.BaseActivity; importorg.androidannotations.annotations.AfterViews; importorg.androidannotations.annotations.EActivity; importorg.androidannotations.annotations.ItemClick; importorg.androidannotations.annotations.ViewById; importorg.w3c.dom.Text; /** * 当前类注释:TextDrawables 效果实例演示 * 项目名:FastDev4Android * 包名:com.chinaztt.fda.test * 作者:江清清 on 15/11/5 22:13 * 邮箱:jiangqqlmj@163.com * QQ: 781931404 * 公司:江苏中天科技软件技术有限公司 */ @EActivity(R.layout.text_drawables_layout) public classTextDrawablesTestActivity extends BaseActivity { @ViewById ListView lv_textdrawable; private String[] mTitles; private LayoutInflater mLayoutInflater; private ColorGenerator mGenerator; @Override protected void onCreate(BundlesavedInstanceState) { super.onCreate(savedInstanceState); mTitles=newString[]{"SAMPLE_RECT" ,"SAMPLE_ROUND_RECT","SAMPLE_ROUND" ,"SAMPLE_RECT_BORDER","SAMPLE_ROUND_RECT_BORDER" ,"SAMPLE_ROUND_BORDER" ,"SAMPLE_MULTIPLE_LETTERS", "SAMPLE_FONT","SAMPLE_SIZE","SAMPLE_ANIMATION","SAMPLE_MISC" }; mGenerator=ColorGenerator.DEFAULT; mLayoutInflater=getLayouInflater(); } @AfterViews public void showLvDrawable(){ lv_textdrawable.setAdapter(newTextAdapter()); } class TextAdapter extends BaseAdapter{ @Override public int getCount() { return mTitles.length; } @Override public Object getItem(int position) { return mTitles[position]; } @Override public long getItemId(int position) { return position; } @Override public View getView(int position, ViewconvertView, ViewGroup parent) { Hondler _Hondler=null; if(convertView==null){ _Hondler=new Hondler(); convertView=mLayoutInflater.inflate(R.layout.text_drawables_item_layout,null); _Hondler.lv_item_img=(ImageView)convertView.findViewById(R.id.lv_item_img); _Hondler.lv_item_text=(TextView)convertView.findViewById(R.id.lv_item_text); convertView.setTag(_Hondler); }else { _Hondler=(Hondler)convertView.getTag(); } _Hondler.lv_item_text.setText(mTitles[position]); Drawable drawable=null; switch (position){ case 0: //SAMPLE_RECT drawable=TextDrawable.builder().buildRect("R",Color.BLUE); break; case 1: //SAMPLE_ROUND_RECT drawable=TextDrawable.builder().buildRoundRect("S",Color.CYAN,10); break; case 2: //SAMPLE_ROUND drawable=TextDrawable.builder().buildRound("圆",Color.LTGRAY); break; case 3: //SAMPLE_RECT_BORDER drawable=TextDrawable.builder().beginConfig() .withBorder(5) .endConfig() .buildRect("粗",Color.RED); break; case 4: //SAMPLE_ROUND_RECT_BORDER drawable=TextDrawable.builder() .beginConfig() .withBorder(5) .endConfig() .buildRoundRect("S",Color.argb(220,122,122,1),10); break; case 5: //SAMPLE_ROUND_BORDER drawable=TextDrawable.builder() .beginConfig().withBorder(5).endConfig() .buildRound("圆",Color.LTGRAY); break; case 6: //SAMPLE_MULTIPLE_LETTERS drawable=TextDrawable.builder() .beginConfig() .fontSize(40) .toUpperCase() .endConfig() .buildRect("AK", mGenerator.getColor("AK")); break; case 7: //SAMPLE_FONT drawable =TextDrawable.builder() .beginConfig() .textColor(Color.BLACK) .useFont(Typeface.SERIF) .bold() .toUpperCase() .endConfig() .buildRect("a", Color.RED); break; case 8: //SAMPLE_SIZE drawable =TextDrawable.builder() .beginConfig() .textColor(Color.BLACK) .fontSize(30) /*size in px */ .bold() .toUpperCase() .endConfig() .buildRect("a", Color.RED); break; case 9: //SAMPLE_ANIMATION TextDrawable.IBuilderbuilder = TextDrawable.builder() .rect(); AnimationDrawableanimationDrawable = new AnimationDrawable(); for (int i = 10; i > 0;i--) { TextDrawable frame =builder.build(String.valueOf(i), mGenerator.getRandomColor()); animationDrawable.addFrame(frame, 1200); } animationDrawable.setOneShot(false); animationDrawable.start(); drawable=(Drawable)animationDrawable; break; case 10: //SAMPLE_MISC drawable=TextDrawable.builder() .buildRect("M", mGenerator.getColor("Misc")); break; } if(drawable!=null){ _Hondler.lv_item_img.setImageDrawable(drawable); } return convertView; } } final static class Hondler{ ImageView lv_item_img; TextView lv_item_text; } @ItemClick(R.id.lv_textdrawable) public void lv_ItemClick(int position){ showToastMsgShort("点击了TextDrawable列表..."); } } ~~~ ## (四).源码分析: 阅读整个该开源代码,其实实现这个效果只有两个类,ColorGenerator和TextDrawable。ColorGenerator为颜色生成引擎比较简单。 4.1.ColorGenerator.java ~~~ packagecom.amulyakhare.textdrawable.util; importjava.util.Arrays; importjava.util.List; importjava.util.Random; /** * @author amulya * @datetime 14 Oct 2014, 5:20 PM */ public classColorGenerator { //使用默认颜色 public static ColorGenerator DEFAULT; //使用物料模式颜色 public static ColorGenerator MATERIAL; static { DEFAULT = create(Arrays.asList( 0xfff16364, 0xfff58559, 0xfff9a43e, 0xffe4c62e, 0xff67bf74, 0xff59a2be, 0xff2093cd, 0xffad62a7, 0xff805781 )); MATERIAL = create(Arrays.asList( 0xffe57373, 0xfff06292, 0xffba68c8, 0xff9575cd, 0xff7986cb, 0xff64b5f6, 0xff4fc3f7, 0xff4dd0e1, 0xff4db6ac, 0xff81c784, 0xffaed581, 0xffff8a65, 0xffd4e157, 0xffffd54f, 0xffffb74d, 0xffa1887f, 0xff90a4ae )); } private final List<Integer> mColors; private final Random mRandom; //使用静态方法 来创建对象 public static ColorGeneratorcreate(List<Integer> colorList) { return new ColorGenerator(colorList); } private ColorGenerator(List<Integer>colorList) { mColors = colorList; mRandom = newRandom(System.currentTimeMillis()); } //生成一个随机颜色 public int getRandomColor() { returnmColors.get(mRandom.nextInt(mColors.size())); } //获取具体的衍生 public int getColor(Object key) { returnmColors.get(Math.abs(key.hashCode()) % mColors.size()); } } ~~~ 4.2.TextDrawable进行构建生成相应的drawble,该采用构建者模式生成,根据我们的需求,来进行相应的构建组装即可。具体代码就不注释了,阅读起来很清晰的。 ~~~ packagecom.amulyakhare.textdrawable; importandroid.graphics.*; importandroid.graphics.drawable.ShapeDrawable; importandroid.graphics.drawable.shapes.OvalShape; importandroid.graphics.drawable.shapes.RectShape; importandroid.graphics.drawable.shapes.RoundRectShape; /** * @author amulya * @datetime 14 Oct 2014, 3:53 PM */ public classTextDrawable extends ShapeDrawable { private final Paint textPaint; private final Paint borderPaint; private static final float SHADE_FACTOR =0.9f; private final String text; private final int color; private final RectShape shape; private final int height; private final int width; private final int fontSize; private final float radius; private final int borderThickness; private TextDrawable(Builder builder) { super(builder.shape); // shape properties shape = builder.shape; height = builder.height; width = builder.width; radius = builder.radius; // text and color text = builder.toUpperCase ?builder.text.toUpperCase() : builder.text; color = builder.color; // text paint settings fontSize = builder.fontSize; textPaint = new Paint(); textPaint.setColor(builder.textColor); textPaint.setAntiAlias(true); textPaint.setFakeBoldText(builder.isBold); textPaint.setStyle(Paint.Style.FILL); textPaint.setTypeface(builder.font); textPaint.setTextAlign(Paint.Align.CENTER); textPaint.setStrokeWidth(builder.borderThickness); // border paint settings borderThickness =builder.borderThickness; borderPaint = new Paint(); borderPaint.setColor(getDarkerShade(color)); borderPaint.setStyle(Paint.Style.STROKE); borderPaint.setStrokeWidth(borderThickness); // drawable paint color Paint paint = getPaint(); paint.setColor(color); } private int getDarkerShade(int color) { return Color.rgb((int)(SHADE_FACTOR *Color.red(color)), (int)(SHADE_FACTOR *Color.green(color)), (int)(SHADE_FACTOR *Color.blue(color))); } @Override public void draw(Canvas canvas) { super.draw(canvas); Rect r = getBounds(); // draw border if (borderThickness > 0) { drawBorder(canvas); } int count = canvas.save(); canvas.translate(r.left, r.top); // draw text int width = this.width < 0 ?r.width() : this.width; int height = this.height < 0 ?r.height() : this.height; int fontSize = this.fontSize < 0 ?(Math.min(width, height) / 2) : this.fontSize; textPaint.setTextSize(fontSize); canvas.drawText(text, width / 2, height/ 2 - ((textPaint.descent() + textPaint.ascent()) / 2), textPaint); canvas.restoreToCount(count); } private void drawBorder(Canvas canvas) { RectF rect = new RectF(getBounds()); rect.inset(borderThickness/2,borderThickness/2); if (shape instanceof OvalShape) { canvas.drawOval(rect, borderPaint); } else if (shape instanceofRoundRectShape) { canvas.drawRoundRect(rect, radius,radius, borderPaint); } else { canvas.drawRect(rect, borderPaint); } } @Override public void setAlpha(int alpha) { textPaint.setAlpha(alpha); } @Override public void setColorFilter(ColorFilter cf){ textPaint.setColorFilter(cf); } @Override public int getOpacity() { return PixelFormat.TRANSLUCENT; } @Override public int getIntrinsicWidth() { return width; } @Override public int getIntrinsicHeight() { return height; } public static IShapeBuilder builder() { return new Builder(); } public static class Builder implementsIConfigBuilder, IShapeBuilder, IBuilder { private String text; private int color; private int borderThickness; private int width; private int height; private Typeface font; private RectShape shape; public int textColor; private int fontSize; private boolean isBold; private boolean toUpperCase; public float radius; private Builder() { text = ""; color = Color.GRAY; textColor = Color.WHITE; borderThickness = 0; width = -1; height = -1; shape = new RectShape(); font =Typeface.create("sans-serif-light", Typeface.NORMAL); fontSize = -1; isBold = false; toUpperCase = false; } public IConfigBuilder width(int width){ this.width = width; return this; } public IConfigBuilder height(intheight) { this.height = height; return this; } public IConfigBuilder textColor(intcolor) { this.textColor = color; return this; } public IConfigBuilder withBorder(intthickness) { this.borderThickness = thickness; return this; } public IConfigBuilder useFont(Typefacefont) { this.font = font; return this; } public IConfigBuilder fontSize(intsize) { this.fontSize = size; return this; } public IConfigBuilder bold() { this.isBold = true; return this; } public IConfigBuilder toUpperCase() { this.toUpperCase = true; return this; } @Override public IConfigBuilder beginConfig() { return this; } @Override public IShapeBuilder endConfig() { return this; } @Override public IBuilder rect() { this.shape = new RectShape(); return this; } @Override public IBuilder round() { this.shape = new OvalShape(); return this; } @Override public IBuilder roundRect(int radius) { this.radius = radius; float[] radii = {radius, radius,radius, radius, radius, radius, radius, radius}; this.shape = newRoundRectShape(radii, null, null); return this; } @Override public TextDrawable buildRect(Stringtext, int color) { rect(); return build(text, color); } @Override public TextDrawablebuildRoundRect(String text, int color, int radius) { roundRect(radius); return build(text, color); } @Override public TextDrawable buildRound(Stringtext, int color) { round(); return build(text, color); } @Override public TextDrawable build(String text,int color) { this.color = color; this.text = text; return new TextDrawable(this); } } public interface IConfigBuilder { public IConfigBuilder width(int width); public IConfigBuilder height(intheight); public IConfigBuilder textColor(intcolor); public IConfigBuilder withBorder(intthickness); public IConfigBuilder useFont(Typefacefont); public IConfigBuilder fontSize(intsize); public IConfigBuilder bold(); public IConfigBuilder toUpperCase(); public IShapeBuilder endConfig(); } public static interface IBuilder { public TextDrawable build(String text,int color); } public static interface IShapeBuilder { public IConfigBuilder beginConfig(); public IBuilder rect(); public IBuilder round(); public IBuilder roundRect(int radius); public TextDrawable buildRect(Stringtext, int color); public TextDrawablebuildRoundRect(String text, int color, int radius); public TextDrawable buildRound(Stringtext, int color); } } ~~~ 我们的项目已经配置集成了TextDrawble的例子,大家以后在做通讯录相关的应用的时候列表中的图标根据根据姓名去设置了。同时欢迎大家去Github站点进行clone或者下载浏览: [https://github.com/jiangqqlmj/FastDev4Android](https://github.com/jiangqqlmj/FastDev4Android) 同时欢迎大家star和fork整个开源快速开发框架项目~
';

消息总线EventBus源码分析以及与Otto框架对比(二十一)

最后更新于:2022-04-01 07:13:05

## (一).前言: 上一篇我们对EventBus的简介和基本使用做了说明,今天我们主要深入的使用EventBus,同时会从源码的角度对于订阅和发送消息做分析,以及和另外的消息总线框架Otto在性能等方面做一个对比分析。  FastDev4Android框架项目地址:[https://github.com/jiangqqlmj/FastDev4Android](https://github.com/jiangqqlmj/FastDev4Android) ## (二).框架简单说明: 通过上一篇文章的介绍,EventBus的使用步骤如下:  *  定义一个事件,用于EventBus的分发。 *  定义订阅者,把该订阅者加入到EventBus中。 *  通过EventBus.post来进行分发事件,告诉订阅者有事情发生了。订阅者接收到信息进行相应处理。 *  使用完成之后,订阅者需要反注册取消订阅。 具体原理图如下: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569c8eb47c3ad.jpg)        订阅者接收到通知的时候会调用相应的函数进行处理事件,在EventBus中一般有以下四种方法来让我们进行处理: 1. onEvent 2. onEventMainThread 3. onEventBackground 4. onEventAsync 这四个订阅方法有很多的相似之处,但是功能上面还是有点不同的,EventBus会通过调用post方法来进行分发消息,让订阅者进行接收,订阅者接收到事件消息是通过上面几个方法来进行接收和处理的。下面我们来对这四个方法的具体使用场景做一个介绍: * onEvent:使用该方法作为订阅函数表示post消息事件和接收消息事件在同一个线程中。 * onEventMainThread: 该方法会在UI  Main线程中运行,接收事件同时会在UI线程中运行,这样我们就可以在该方法中直接更新UI * onEventBackground:使用该方法,如果事件是在UI Main线程发出来,该方法会在子线程中执行,如果事件是从子线程中发出来,该onEventBackground方法会在子线程中执行。 * onEventAsync:使用该方法,会在创建新的子线程中执行onEventAsync 那么现在订阅的函数方法有四个,我们怎么会知道具体调用哪个方法呢?OK我们看一篇文章:我们会先创建一个事件类,然后进行post发送该对象,在订阅方法中接收,注入哪个函数的参数就是该发送过来的对象。这样我们应该清楚了吧,那是根据传进来的事件对象参数来进行判断的。具体我们看实例: ## (三).调用实例: 3.1.实现需求:我们现在创建三个Event事件类,第二个Activity中进行发送,在订阅者Activity中进行接收订阅方法如下:             ~~~ /** * 收到消息 进行相关处理 * @param event */ public voidonEventMainThread(TestEventFirst event) { Log.d("zttjiangqq","onEventMainThread收到消息:"+event.getMsg()); textView_one.setText(event.getMsg()); //showToastMsgShort(event.getMsg()); } /** * 收到消息 进行相关处理 * @param event */ public voidonEventMainThread(TestEventSecond event) { Log.d("zttjiangqq","onEventMainThread收到消息:"+event.getMsg()); textView_two.setText(event.getMsg()); //showToastMsgShort(event.getMsg()); } /** * 收到消息 进行相关处理 * @param event */ public voidonEventMainThread(TestEventThird event) { Log.d("zttjiangqq","onEventMainThread收到消息:"+event.getMsg()); textView_third.setText(event.getMsg()); //showToastMsgShort(event.getMsg()); } ~~~ 3.2.演示效果如下: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569c8eb4996ff.jpg)  ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569c8eb508661.jpg) ## (四).源码解析:  以上主要为EventBus的主要使用,现在开始我们对于EventBus的注册和发送两个模块从源码的角度来走一下。 4.1.EventBus对象获取:我们一般使用单例模式获取。保证对象唯一性。 ~~~ /** * 采用单例模式获取EventBus实例对象 一般我们获取EventBus对象 就是采用这种方式,不建议直接new * @return */ public static EventBus getDefault() { if (defaultInstance == null) { synchronized (EventBus.class) { if (defaultInstance == null) { defaultInstance = newEventBus(); } } } return defaultInstance; } ~~~ 4.2.订阅模块:入口,进行订阅注册 ~~~ public void register(Object subscriber) { register(subscriber, false, 0); } ~~~ *   subscriber:需要注册的订阅者, *  sticky:是否为粘性,这边默认为false, *   priority:事件的优先级,默认为0 下面我们来具体看一下register(subscriber, false, 0)方法具体实现的功能: ~~~ private synchronizedvoid register(Object subscriber, boolean sticky, int priority) { List<SubscriberMethod>subscriberMethods =subscriberMethodFinder.findSubscriberMethods(subscriber.getClass()); for (SubscriberMethod subscriberMethod: subscriberMethods) { subscribe(subscriber,subscriberMethod, sticky, priority); } } ~~~ 该函数中会通过findSubscriberMethods()来获取所有订阅的方法,具体主要的步骤我这边已经进行注释了 ~~~ /** * 进行查找订阅者中所有订阅的方法 * @param subscriberClass * @return 所有订阅的方法的集合 */ List<SubscriberMethod>findSubscriberMethods(Class<?> subscriberClass) { String key = subscriberClass.getName(); List<SubscriberMethod>subscriberMethods; //从缓存中获取订阅的方法,第一次使用肯定缓存中不存在 synchronized (methodCache) { subscriberMethods =methodCache.get(key); } if (subscriberMethods != null) { return subscriberMethods; } //订阅方法的集合 subscriberMethods = newArrayList<SubscriberMethod>(); Class<?> clazz = subscriberClass; HashMap<String, Class>eventTypesFound = new HashMap<String, Class>(); StringBuilder methodKeyBuilder = newStringBuilder(); while (clazz != null) { String name = clazz.getName(); if(name.startsWith("java.") || name.startsWith("javax.") ||name.startsWith("android.")) { // Skip system classes, thisjust degrades performance // 这边直接跳过了系统类,因为系统类中 普通开发者不会使用在系统类中使用EventBus,所以就忽略处理了,不然会降低性能 break; } // Starting with EventBus 2.2 weenforced methods to be public (might change with annotations again) try { // This is faster thangetMethods, especially when subscribers a fat classes like Activities // 通过反射来获取当前类中的所有方法 Method[] methods =clazz.getDeclaredMethods(); // 正式开始查询所有订阅的方法 filterSubscriberMethods(subscriberMethods, eventTypesFound,methodKeyBuilder, methods); } catch (Throwable th) { th.printStackTrace(); // Workaround forjava.lang.NoClassDefFoundError, see https://github.com/greenrobot/EventBus/issues/149 Method[] methods =subscriberClass.getMethods(); subscriberMethods.clear(); eventTypesFound.clear(); filterSubscriberMethods(subscriberMethods, eventTypesFound,methodKeyBuilder, methods); break; } clazz = clazz.getSuperclass(); } //抛出异常,订阅者没有实现onEvent开头的公共方法 if (subscriberMethods.isEmpty()) { throw newEventBusException("Subscriber " + subscriberClass + " has nopublic methods called " + ON_EVENT_METHOD_NAME); } else { //订阅的方法存在,同时加入缓存 synchronized (methodCache) { methodCache.put(key,subscriberMethods); } return subscriberMethods; } } ~~~ 然后调用filterSubscriberMethods()进行过滤,把订阅方法加入到集合中 ~~~ /** * 查询订阅的方法,查到方法,方法加入到subScriberMethods * @param subscriberMethods * @param eventTypesFound * @param methodKeyBuilder * @param methods */ private voidfilterSubscriberMethods(List<SubscriberMethod> subscriberMethods, HashMap<String, Class> eventTypesFound, StringBuildermethodKeyBuilder, Method[] methods) { //遍历类中的所有方法 for (Method method : methods) { String methodName =method.getName(); //过滤onEvent开头的方法 if(methodName.startsWith(ON_EVENT_METHOD_NAME)) { //返回方法修饰符 例如public,private,protected int modifiers =method.getModifiers(); Class<?> methodClass =method.getDeclaringClass(); if ((modifiers &Modifier.PUBLIC) != 0 && (modifiers & MODIFIERS_IGNORE) == 0) { //订阅方法必须为public类型 //进行获取方法的参数类型 Class<?>[]parameterTypes = method.getParameterTypes(); if (parameterTypes.length== 1) { //进行获取线程模式类型 ThreadMode threadMode =getThreadMode(methodClass, method, methodName); if (threadMode == null){ continue; } //取出当前传入的订阅者 Class<?>eventType = parameterTypes[0]; //methodKeyBuilder key="0"."methodName".">"."eventType_Name" methodKeyBuilder.setLength(0); methodKeyBuilder.append(methodName); methodKeyBuilder.append('>').append(eventType.getName()); String methodKey =methodKeyBuilder.toString(); Class methodClassOld =eventTypesFound.put(methodKey, methodClass); if (methodClassOld ==null || methodClassOld.isAssignableFrom(methodClass)) { // Only add if notalready found in a sub class //构建一个订阅方法的对象(里面存入方法名,线程模式类型,事件类型),加入到订阅方法集合中 subscriberMethods.add(new SubscriberMethod(method, threadMode,eventType)); } else { // Revert the put,old class is further down the class hierarchy eventTypesFound.put(methodKey, methodClassOld); } } } else if(!skipMethodVerificationForClasses.containsKey(methodClass)) { Log.d(EventBus.TAG,"Skipping method (not public, static or abstract): " + methodClass +"." + methodName); } } } } ~~~ 上面已经进行获取了所有的订阅函数,那么现在开始就可以进行订阅了,让我们来查看subscribe()方法做的功能操作:  ~~~ /** * 开始进行为订阅者 注册相关的订阅方法 * @param subscriber 订阅者 * @param subscriberMethod 订阅的方法 * @param sticky 是否为粘性 * @param priority 优先级 */ private void subscribe(Object subscriber,SubscriberMethod subscriberMethod, boolean sticky, int priority) { //通过订阅方法中进行获取订阅方法的类型 Class<?> eventType =subscriberMethod.eventType; //通过订阅事件的类型 进行获取所有的订阅信息(有订阅者对象,订阅方法,优先级) CopyOnWriteArrayList<Subscription> subscriptions =subscriptionsByEventType.get(eventType); //进行创建一个订阅者 Subscription newSubscription = newSubscription(subscriber, subscriberMethod, priority); if (subscriptions == null) { //如果当前的事件类型不存在订阅信息,那么就创建一个订阅信息集合 subscriptions = newCopyOnWriteArrayList<Subscription>(); //同时把当前的订阅信息加入到该订阅中 subscriptionsByEventType.put(eventType, subscriptions); } else { if(subscriptions.contains(newSubscription)) { //抛出异常,该订阅者已经注册过该事件类中 throw newEventBusException("Subscriber " + subscriber.getClass() + "already registered to event " + eventType); } } // Starting with EventBus 2.2 weenforced methods to be public (might change with annotations again) //subscriberMethod.method.setAccessible(true); // 优先级判断,进行排序 int size = subscriptions.size(); for (int i = 0; i <= size; i++) { if (i == size ||newSubscription.priority > subscriptions.get(i).priority) { subscriptions.add(i,newSubscription); break; } } //将当前的事件加入到订阅者列表中 List<Class<?>>subscribedEvents = typesBySubscriber.get(subscriber); if (subscribedEvents == null) { subscribedEvents = newArrayList<Class<?>>(); typesBySubscriber.put(subscriber,subscribedEvents); } subscribedEvents.add(eventType); //是否粘性判断 if (sticky) { if (eventInheritance) { // Existing sticky events ofall subclasses of eventType have to be considered. // Note: Iterating over allevents may be inefficient with lots of sticky events, // thus data structure shouldbe changed to allow a more efficient lookup // (e.g. an additional mapstoring sub classes of super classes: Class -> List<Class>). Set<Map.Entry<Class<?>, Object>> entries =stickyEvents.entrySet(); for(Map.Entry<Class<?>, Object> entry : entries) { Class<?>candidateEventType = entry.getKey(); if(eventType.isAssignableFrom(candidateEventType)) { Object stickyEvent =entry.getValue(); checkPostStickyEventToSubscription(newSubscription, stickyEvent); } } } else { Object stickyEvent =stickyEvents.get(eventType); checkPostStickyEventToSubscription(newSubscription, stickyEvent); } } } ~~~ OK完成以上步骤,我们就大体完成了订阅注册工作,下面就是需要分析一下post的流程: 4.3.主要先看post主函数: ~~~ /** * 向EventBus中发送消息事件对象 * @param event */ public void post(Object event) { PostingThreadState postingState =currentPostingThreadState.get(); //把消息加入到事件队列中 List<Object> eventQueue =postingState.eventQueue; eventQueue.add(event); if (!postingState.isPosting) { postingState.isMainThread =Looper.getMainLooper() == Looper.myLooper(); postingState.isPosting = true; if (postingState.canceled) { throw newEventBusException("Internal error. Abort state was not reset"); } try { //当消息队列不为空的时候,进行这正式发送消息,采用循环,把队列中所有的消息发送出去 while (!eventQueue.isEmpty()) { postSingleEvent(eventQueue.remove(0), postingState); } } finally { postingState.isPosting = false; postingState.isMainThread =false; } } } 然后进行发送功能,调用postSingleEvent()函数方法: //消息发送:发送单个事件消息 private void postSingleEvent(Object event,PostingThreadState postingState) throws Error { Class<?> eventClass =event.getClass(); boolean subscriptionFound = false; if (eventInheritance) { List<Class<?>>eventTypes = lookupAllEventTypes(eventClass); int countTypes = eventTypes.size(); for (int h = 0; h < countTypes;h++) { Class<?> clazz =eventTypes.get(h); subscriptionFound |=postSingleEventForEventType(event, postingState, clazz); } } else { subscriptionFound =postSingleEventForEventType(event, postingState, eventClass); } if (!subscriptionFound) { if (logNoSubscriberMessages) { Log.d(TAG, "No subscribersregistered for event " + eventClass); } if (sendNoSubscriberEvent&& eventClass != NoSubscriberEvent.class && eventClass !=SubscriberExceptionEvent.class) { post(newNoSubscriberEvent(this, event)); } } } ~~~ 接着进行消息过滤postSingleEventForEventType()方法 ~~~ /** * 进行该特定的Event发送相应的消息 * @param event 事件消息 * @param postingState * @param eventClass * @return */ private booleanpostSingleEventForEventType(Object event, PostingThreadState postingState,Class<?> eventClass) { CopyOnWriteArrayList<Subscription> subscriptions; synchronized (this) { subscriptions =subscriptionsByEventType.get(eventClass); } if (subscriptions != null &&!subscriptions.isEmpty()) { for (Subscription subscription :subscriptions) { postingState.event = event; postingState.subscription =subscription; boolean aborted = false; try { //发生消息给订阅者 postToSubscription(subscription, event, postingState.isMainThread); aborted =postingState.canceled; } finally { postingState.event = null; postingState.subscription =null; postingState.canceled =false; } if (aborted) { break; } } return true; } return false; } ~~~ 最终这边有一个核心的方法:postToSubscription()来进行post消息 ~~~ /** * 进行发送消息,同时根据发送过来的线程类型类型,发送消息给特定的订阅方法来进行执行 * @param subscription 订阅者 * @param event 执行事件 * @param isMainThread 是否为主线程 */ private voidpostToSubscription(Subscription subscription, Object event, booleanisMainThread) { switch(subscription.subscriberMethod.threadMode) { case PostThread: //直接在本线程中调用订阅函数 invokeSubscriber(subscription,event); break; case MainThread: if (isMainThread) { //如果是主线程,直接调用订阅函数 invokeSubscriber(subscription, event); } else { //如果不是主线程,通过handler进行处理 mainThreadPoster.enqueue(subscription, event); } break; case BackgroundThread: if (isMainThread) { //如果是主线程,采用runnable 中调用 backgroundPoster.enqueue(subscription, event); } else { //子线程,直接调用 invokeSubscriber(subscription, event); } break; case Async: //加入到子线程中进行发送 asyncPoster.enqueue(subscription, event); break; default: throw newIllegalStateException("Unknown thread mode: " +subscription.subscriberMethod.threadMode); } } ~~~ 4.4.取消注册(反注册)主要查看unregister()方法 ~~~ /** * 订阅者取消注册,反注册 * @param subscriber */ public synchronized void unregister(Object subscriber) { List<Class<?>> subscribedTypes = typesBySubscriber.get(subscriber); if (subscribedTypes != null) { for (Class<?> eventType : subscribedTypes) { unsubscribeByEventType(subscriber, eventType); } typesBySubscriber.remove(subscriber); } else { Log.w(TAG, "Subscriber to unregister was not registered before: " + subscriber.getClass()); } } ~~~ OK,到这边基本上完成EventBus的register和post的流程的讲解,关于这个核心类EventBus的注释过的类文件已经上传了大家可以通过该地址进行下载:[EventBus注释过的类文件](http://download.csdn.net/detail/jiangqq781931404/9239001) ## (五).和Otto消息总线框架对比: Otto是Android中另外一个消息总线库,它其实是EventBus的变种,该和EventBus有一些相同的方法(例如:register,post,unregister…),但是这两者之间也有一些不同之处如下: | - | EventBus| Otto| | --|--|--| |声明事件处理方法|命名约定| 注解 | |事件继承| YES| YES| |订阅继承| YES| N | |缓存事件| YES,sticky events|NO | |事件生产|NO |YES | |子线程事件传输| YES(Default)|YES | |主线程事件传输|YES |NO | |后台线程事件传输|YES |NO | |异步线程事件传输|YES | NO| 除了以上功能不同以外,这边还有性能上面的差异。为了测试性能问题,我们clone当前EventBus项目的时候,会发现有一个EventBusPerformance项目,我们可以使用的不同场景来比较。 基于下表结果显示,每一个测试方面EventBus的性能都大于Otto | - | EventBus| Otto| |--|--|--| |在Android2.3模拟器发送1000个事件| 快70%| | | S3Android4.0系统,发送1000个事件| 快110%| | | Android2.3模拟器,注册1000个订阅者| 快10%| | | S3 Android4.0系统,注册1000个订阅者| 快70%| | | Android2.3模拟器,注册订阅者冷启动| 快350%| | | S3 Android4.04注册订阅者冷启动| 几乎一样| | 通过对比发现EventBus无论在功能上面还是性能上面,远远超过Otto消息总线框架,所以我们建议使用EventBus消息总线框架。 到此我们的EventBus专题内容已经全部讲完了,相信大家在这个专题中能对EventBus会有一个比较全面的了解,同时也能够简单的了解实现的原理以及相关逻辑。 我们的项目已经配置集成了消息总线EventBus的例子.欢迎大家去Github站点进行clone或者下载浏览:[https://github.com/jiangqqlmj/FastDev4Android](https://github.com/jiangqqlmj/FastDev4Android) 同时欢迎大家star和fork整个开源快速开发框架项目~
';

消息总线EventBus的基本使用(二十)

最后更新于:2022-04-01 07:13:03

## (一).前言: 今天我们的项目继续更新,今天我们主要讲解消息总线EventBus的基本使用方法,后面一篇我们会从源码的角度稍微分析一下实现过程。 FastDev4Android框架项目地址:[https://github.com/jiangqqlmj/FastDev4Android](https://github.com/jiangqqlmj/FastDev4Android) ## (二).简介: 以前我们做组件间的消息分发更新,一般会采用观察者模式,或者接口数据回调的相关方式。但是这样的做法虽然可以解决我们的问题,但是组件之间的耦合比较严重,而且代码也不易阅读和相关维护。为了解决这样的问题我们可以使用消息总线EventBus框架。 EventBus是一款针对Android优化的发布/订阅事件总线。主要功能是替代Intent,Handler,BroadCast在Fragment,Activity,Service,线程之间传递消息.优点是开销小,代码更优雅。以及将发送者和接收者解耦。EventBus开源站点地址:[https://github.com/greenrobot/EventBus](https://github.com/greenrobot/EventBus)。 整个订阅和接受的架构如下图: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569c8eb4112fa.jpg) EventBus的特点如下: * 简化组件间的消息通信 * 使得代码更加简洁 * 速度很快 * jar包非常小,不到50K * 还有一些有点例如线程之间的通信,优先级等 ## (三).使用方式 3.1.AndroidStudio进行Gradle配置如下: ~~~ compile 'de.greenrobot:eventbus:2.4.0' ~~~ 3.2.事件对象定义 ~~~ publicclass MessageEvent { /* Additional fields if needed */ } ~~~ 3.3.在接收页面进行注册     ~~~ eventBus.register(this); ~~~ 3.4.接收消息方法实现         ~~~ public voidonEvent(AnyEventType event) {/* Do something */}; ~~~ 3.5.消息发送 ~~~ eventBus.post(event); ~~~        OK上面是官方的使用说明,现在我们来具体使用一个实例来展示一下EventBus的基本使用。 ## (四).具体事例 4.1.实现需求:在第一个Activity中有一个按钮和一个TextView,然后点击按钮打开第二个Activity,在第二个Activity中有一个按钮,点击按钮关闭当前第二个Activity,同时消息回调到第一个Activity中,在TextView中进行显示。   ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569c8eb42aaf9.jpg) 4.2.我们这边需要两个Activity布局 ~~~ <?xmlversion="1.0" encoding="utf-8"?> <LinearLayoutxmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical"android:layout_width="match_parent" android:layout_height="match_parent"> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="点击打开第二个Activity" android:id="@+id/button_one"/> <TextView android:layout_width="wrap_content" android:layout_height="45dp" android:text="这边显示消息内容..." android:id="@+id/textView_one" /> </LinearLayout> ~~~ ~~~ <?xmlversion="1.0" encoding="utf-8"?> <LinearLayoutxmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical"android:layout_width="match_parent" android:layout_height="match_parent"> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="第二个Activity的按钮" android:id="@+id/button_two"/> <TextView android:layout_width="wrap_content" android:layout_height="45dp" android:text="当前为第二个Activity" android:id="@+id/textView_two" /> </LinearLayout> ~~~ 4.3.创建一个事件管理类:TestEventFirst.java ~~~ packagecom.chinaztt.fda.event; /** * 当前类注释:EventBus测试 First事件类 * 项目名:FastDev4Android * 包名:com.chinaztt.fda.event * 作者:江清清 on 15/11/3 14:25 * 邮箱:jiangqqlmj@163.com * QQ: 781931404 * 公司:江苏中天科技软件技术有限公司 */ public classTestEventFirst { private String msg; public String getMsg() { return msg; } public void setMsg(String msg) { this.msg = msg; } public TestEventFirst(String msg){ this.msg=msg; } } ~~~ 4.4:注册和取消注册 使用EventBus.getDefault().register(this);进行注册 使用EventBus.getDefault().unregister(this);进行取消注册 4.5.消息发送 使用 EventBus.getDefault().post(new TestEventFirst("我是第二个Activity回传的信息...."));进行消息发送 4.6.消息接收 在注册的Activity中进行重写onEventMainThread()方法来进行处理接收消息(除了这个方法以外,还有另外三个方法,具体我们会在下一篇文章中进行介绍) ~~~ /** * 收到消息 进行相关处理 * @param event */ public voidonEventMainThread(TestEventFirst event) { textView_one.setText(event.getMsg()); showToastMsgShort(event.getMsg()); } ~~~    其中方法中的参数TestEventFirst就是发送过来的消息类,具体发送的消息全部已经封装在里面了。我们只需要使用event对象进行获取处理即可。 4.7.完整第一个Activity和第二个Activity代码如下: ~~~ packagecom.chinaztt.fda.test; importandroid.os.Bundle; importandroid.view.View; importandroid.widget.Button; importandroid.widget.TextView; importandroid.widget.Toast; importcom.chinaztt.fda.event.TestEventFirst; importcom.chinaztt.fda.ui.R; importcom.chinaztt.fda.ui.base.BaseActivity; importcom.chinaztt.fda.utils.Log; importorg.androidannotations.annotations.Click; importorg.androidannotations.annotations.EActivity; importorg.androidannotations.annotations.ViewById; importorg.w3c.dom.Text; importde.greenrobot.event.EventBus; /** * 当前类注释:EventBus组件间数据通信实例 * 项目名:FastDev4Android * 包名:com.chinaztt.fda.test * 作者:江清清 on 15/11/3 13:14 * 邮箱:jiangqqlmj@163.com * QQ: 781931404 * 公司:江苏中天科技软件技术有限公司 */ @EActivity public classEventBusTestActivity extendsBaseActivity{ Button button_one; TextView textView_one; @Override protected void onCreate(BundlesavedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.event_bus_test_layout); EventBus.getDefault().register(this); button_one=(Button)this.findViewById(R.id.button_one); textView_one=(TextView)this.findViewById(R.id.textView_one); button_one.setOnClickListener(newView.OnClickListener() { @Override public void onClick(View v) { openActivity(EventBusTestTwoActivity_.class); } }); } /** * 收到消息 进行相关处理 * @param event */ public voidonEventMainThread(TestEventFirst event) { textView_one.setText(event.getMsg()); showToastMsgShort(event.getMsg()); } @Override protected void onDestroy() { super.onDestroy(); EventBus.getDefault().unregister(this); } } ~~~ ~~~ packagecom.chinaztt.fda.test; importandroid.os.Bundle; importandroid.view.View; importandroid.widget.Button; importcom.chinaztt.fda.event.TestEventFirst; importcom.chinaztt.fda.ui.R; importcom.chinaztt.fda.ui.base.BaseActivity; importorg.androidannotations.annotations.Click; importorg.androidannotations.annotations.EActivity; importorg.androidannotations.annotations.ViewById; importde.greenrobot.event.EventBus; /** * 当前类注释: * 项目名:FastDev4Android * 包名:com.chinaztt.fda.test * 作者:江清清 on 15/11/3 14:25 * 邮箱:jiangqqlmj@163.com * QQ: 781931404 * 公司:江苏中天科技软件技术有限公司 */ @EActivity public classEventBusTestTwoActivity extends BaseActivity { Button button_two; @Override protected void onCreate(BundlesavedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.event_bus_test_two_layout); button_two=(Button)this.findViewById(R.id.button_two); button_two.setOnClickListener(newView.OnClickListener() { @Override public void onClick(View v) { EventBus.getDefault().post(new TestEventFirst("我是第二个Activity回传的信息....")); EventBusTestTwoActivity.this.finish(); } }); } } ~~~ 到此我们的EventBus的基本使用已经讲完了,看一下上面的效果演示,具体深入详解以及其他的几个方法的介绍和相关源代码分析会在下一篇文章中进行讲解。 我们的项目已经配置集成了消息总线EventBus的例子.欢迎大家去Github站点进行clone或者下载浏览:[https://github.com/jiangqqlmj/FastDev4Android](https://github.com/jiangqqlmj/FastDev4Android) 同时欢迎大家star和fork整个开源快速开发框架项目~
';

Android MVP开发模式详解(十九)

最后更新于:2022-04-01 07:13:01

## (一).前言: 今天我们的项目继续更新,今天我们主要讲解MVP开发模式以及具体实例。 FastDev4Android框架项目地址:[https://github.com/jiangqqlmj/FastDev4Android](https://github.com/jiangqqlmj/FastDev4Android) ## (二).简介: MVP(Model ViewPresenter)模式是著名的MVC(Model ViewController)模式的一个演化版本,目前它在Android应用开发中越来越重要了。初看起来我们会感觉增加了很多类接口代码看起来更加清晰。 MVP模式可以分离显示层和逻辑层,所以功能接口如何工作与功能的展示可以实现分离,MVP模式理想化地可以实现同一份逻辑代码搭配不同的显示界面。不过MVP不是一个结构化的模式,它只是负责显示层而已,任何时候都可以在自己的项目结构中使用MVP模式。(不局限于Android项目开发) 因为MVP其实就是从MVC模式演化产生的,那么我们先看一下著名的MVC模式: *      View:对应于布局文件 *       Model:业务逻辑和实体模型 *     Controller:控制器,Android中对应于Activity 对应的交互图如下: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569c8eb2e03bb.jpg) 虽然Android系统应用开发本身是遵循MVC开发模式的,但是我们仔细看一下View层和Activity,具体view布局文件中的数据绑定和事件处理的方法代码都是冗余在Activity中的,所以我们经常看可以看到Activity类动不动就是少则九百行,多则上千甚至几千行。那么现在的演化升级版本的MVP的模式又是怎么样的呢?MVP模式会引入 Presenter层,该机型复杂完成View层和Model层的交互,那么具体MVP对应如下: *       View:View通常来说是由Activity实现的,它会包含一个Presenter的引用,View要做的就只是在每次有接口调用的时候(比如按钮点击后)调用Presenter的方法。 *        Model:业务逻辑和实体模型 *      Presenter:主要作为沟通View和Model的桥梁,它从Model层检索数据后,返回给View层,但是不像MVC结构,因为它也可以决定与View层的交互操作。 数据交互图如下: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569c8eb2f041e.jpg) 观察上面两个模式的交互图,是不是MVP模式更加清晰简单啊! ## (三).MVC和MVP区别: 我们来具体看一下下面两张对比,就可以看来具体区别了: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569c8eb314fec.jpg) 观察上图我们可以发现MVP模式中,View和Model的交互是通过Presenter来进行完成,这样统一管理,逻辑会更加清晰。 ## (四).MVP模式例子讲解:    4.1.具体实现功能需求:我们是用MVP模式来进行实现用户登录操作. 4.2.例子实例如下:  ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569c8eb33930e.jpg) 4.3.项目代码框架如下: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569c8eb3c006b.jpg) ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569c8eb3d2e73.jpg) ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569c8eb3e0957.jpg) ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569c8eb3f226f.jpg) 4.4.代码具体实现: 4.4.1.Model层:Bean类(Entity),PersonBean类,然后在业务逻辑类中有登录方法,同时把登录成功状态回调接口传入进入,具体如下: ~~~ packagecom.chinaztt.fda.entity; /** * 当前类注释:用户信息实体类 * 项目名:FastDev4Android * 包名:com.chinaztt.fda.entity * 作者:江清清 on 15/10/27 14:13 * 邮箱:jiangqqlmj@163.com * QQ: 781931404 * 公司:江苏中天科技软件技术有限公司 */ public class PersonBean { private String username; private String password; public PersonBean() { } public PersonBean(String username, Stringpassword) { this.username = username; this.password = password; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } @Override public String toString() { return "PersonBean{" + "username='" +username + '\'' + ", password='" +password + '\'' + '}'; } } ~~~ ~~~ public interface IPersonBiz { void login(String username,Stringpassword,LoginRequestCallBack valueCallBack); } ~~~ ~~~ packagecom.chinaztt.fda.biz.imp; importcom.chinaztt.fda.biz.IPersonBiz; importcom.chinaztt.fda.biz.LoginRequestCallBack; importcom.chinaztt.fda.entity.PersonBean; importcom.chinaztt.fda.utils.Log; /** * 当前类注释:用户相关业务逻辑实现类 * 项目名:FastDev4Android * 包名:com.chinaztt.fda.biz.imp * 作者:江清清 on 15/10/27 16:33 * 邮箱:jiangqqlmj@163.com * QQ: 781931404 * 公司:江苏中天科技软件技术有限公司 */ public class PersonBizImp implements IPersonBiz{ private static final String TAG="PersonBizImp"; @Override public void login(final String username,final String password, final LoginRequestCallBack valueCallBack) { Log.d(TAG,"username:"+username+",password:"+password); new Thread(new Runnable() { @Override public void run() { try { Thread.sleep(4500); } catch (InterruptedExceptione) { e.printStackTrace(); } //进行开始登录,这边应该进行请求服务器,进行数据验证 if(username.equals("jiangqq")&&password.equals("12345")){ valueCallBack.loginSuccess(new PersonBean(username,password)); }else{ valueCallBack.loginFailed(); } } }).start(); } } ~~~ ~~~ packagecom.chinaztt.fda.biz; importcom.chinaztt.fda.entity.PersonBean; /** * 当前类注释:登录请求结果回调 * 项目名:FastDev4Android * 包名:com.chinaztt.fda.biz * 作者:江清清 on 15/10/27 19:50 * 邮箱:jiangqqlmj@163.com * QQ: 781931404 * 公司:江苏中天科技软件技术有限公司 */ public interface LoginRequestCallBack { //登录成功回调方法 void loginSuccess(PersonBean personBean); //登录失败回调方法 void loginFailed(); } ~~~ 4.4.2.View层:该通过Presenter与View进行交互,这边需要定义一个接口ILoginView: ~~~ packagecom.chinaztt.fda.ui.view; importcom.chinaztt.fda.entity.PersonBean; /** * 当前类注释:登录页面 相关操作 功能接口 * 项目名:FastDev4Android * 包名:com.chinaztt.fda.ui.view * 作者:江清清 on 15/10/27 16:35 * 邮箱:jiangqqlmj@163.com * QQ: 781931404 * 公司:江苏中天科技软件技术有限公司 */ public interface ILoginView { //获取用户名 String getUserName(); //获取密码 String getPassword(); void showSuccessInfo(PersonBeanpersonBean); void showFailedInfo(); } ~~~ 有了上面的接口之后,我们就需要写我们的实现类Activity了,就非常简单了 ~~~ packagecom.chinaztt.fda.test; importandroid.os.Bundle; importandroid.view.View; importandroid.widget.Button; importandroid.widget.EditText; importcom.chinaztt.fda.entity.PersonBean; importcom.chinaztt.fda.presenter.LoginPresenter; importcom.chinaztt.fda.ui.R; importcom.chinaztt.fda.ui.base.BaseActivity; importcom.chinaztt.fda.ui.view.ILoginView; importcom.chinaztt.fda.utils.Log; importorg.androidannotations.annotations.EActivity; /** * 当前类注释:MVP开发模式实例 * 项目名:FastDev4Android * 包名:com.chinaztt.fda.test * 作者:江清清 on 15/10/27 13:38 * 邮箱:jiangqqlmj@163.com * QQ: 781931404 * 公司:江苏中天科技软件技术有限公司 */ @EActivity public class MVPTestActivity extends BaseActivity implements ILoginView{ private static final String TAG="MVPTestActivity"; private EditText ed_username; private EditText ed_password; private Button btn_login; private LoginPresenter mLoginPresenter; @Override protected void onCreate(BundlesavedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.mvp_test_layout); ed_username=(EditText)this.findViewById(R.id.ed_username); ed_password=(EditText)this.findViewById(R.id.ed_password); btn_login=(Button)this.findViewById(R.id.btn_login); mLoginPresenter=newLoginPresenter(this); btn_login.setOnClickListener(newView.OnClickListener() { @Override public void onClick(View v) { mLoginPresenter.loginSystem(); } }); } /** * 进行返回用户名信息 * @return */ @Override public String getUserName() { returned_username.getText().toString().trim(); } /** * 进行返回用户密码信息 * @return */ @Override public String getPassword() { returned_password.getText().toString().trim(); } /** * 登录成功 回调 * @param personBean */ @Override public void showSuccessInfo(PersonBeanpersonBean) { Log.d(TAG,"showSuccessInfo:"+personBean.toString()); showToastMsgShort("登录成功:"+personBean.toString()); } /** * 登录失败 回调 */ @Override public void showFailedInfo() { Log.d(TAG,"showFailedInfo..."); showToastMsgShort("登录失败..."); } } ~~~ 最后还少一个交互桥梁Presenter: 4.4.3.Presenter层:作为Model和View之间的交互桥梁,在本例中进行执行登录操作,然后去Model业务中执行登录,最后把登录结果信息返回给View层,就是这么简单: ~~~ packagecom.chinaztt.fda.presenter; importandroid.os.Handler; importcom.chinaztt.fda.biz.IPersonBiz; importcom.chinaztt.fda.biz.LoginRequestCallBack; importcom.chinaztt.fda.biz.imp.PersonBizImp; importcom.chinaztt.fda.entity.PersonBean; importcom.chinaztt.fda.ui.view.ILoginView; importcom.chinaztt.fda.utils.Log; /** * 当前类注释:负责完成登录界面View于Model(IPersonBiz)间的交互 * 项目名:FastDev4Android * 包名:com.chinaztt.fda.presenter * 作者:江清清 on 15/10/27 16:36 * 邮箱:jiangqqlmj@163.com * QQ: 781931404 * 公司:江苏中天科技软件技术有限公司 */ public class LoginPresenter { private static final String TAG="LoginPresenter"; private ILoginView mLoginView; private IPersonBiz mPersonBiz; private Handler mHandler=new Handler(); public LoginPresenter(ILoginView view) { mLoginView = view; mPersonBiz = new PersonBizImp(); } public void loginSystem(){ mPersonBiz.login(mLoginView.getUserName(), mLoginView.getPassword(), newLoginRequestCallBack() { /** * 登录成功 * @param personBean */ @Override public void loginSuccess(finalPersonBean personBean) { Log.d(TAG,"登录成功:" + personBean.toString()); mHandler.post(new Runnable() { @Override public void run() { mLoginView.showSuccessInfo(personBean); } }); } /** * 登录失败 */ @Override public void loginFailed() { Log.d(TAG,"登录失败..."); mHandler.post(new Runnable() { @Override public void run() { mLoginView.showFailedInfo();; } }); } }); } } ~~~ 到此我们的MVP模式的例子就大体完成了,看一下上面的效果演示就OK了。 我们的项目已经配置集成了MVPDemo的例子.欢迎大家去Github站点进行clone或者下载浏览:[https://github.com/jiangqqlmj/FastDev4Android](https://github.com/jiangqqlmj/FastDev4Android) 同时欢迎大家star和fork整个开源快速开发框架项目~
';