自定义层叠布局StackLayout
来源:互联网 发布:数据分析临界值 编辑:程序博客网 时间:2024/05/23 19:15
一.效果
1.层叠显示,通过xml属性可控制Y轴偏移量,X轴偏移量,缩放比例。
<?xml version="1.0" encoding="utf-8"?><resources> <declare-styleable name="StackLayout"> //Y轴方向的偏移量,负数向上偏移,正数向下偏移 <attr name="offsetY" format="dimension"/> //X轴方向的偏移量,负数向左偏移,正数向右偏移 <attr name="offseetScale" format="integer"/> //缩放比的偏移量,范围为0-100, <attr name="offsetX" format="dimension"/> </declare-styleable></resources>
2.可拖动,自动复位。拖动时有动画效果。
3.支持通过调用函数的方式飞出,且方向可以自定义。
//函数声明 /** * 自动飞出 * @param left true表示从左边飞出,否则从右边 * @param up true表示从上边放飞出,否则从下边飞出 */ public void takeOff(boolean left,boolean up){ if(getChildCount()!=0){ mSelectIndex=getChildCount()-1; autoDismissOrRestore(left?-2000:2000,up?-2000:2000); } } //使用 @Override public void onClick(View v) { switch (v.getId()){ case R.id.btn1: gallery.takeOff(true,true); break; case R.id.btn2: gallery.takeOff(true,false); break; case R.id.btn3: gallery.takeOff(false,true); break; case R.id.btn4: gallery.takeOff(false,false); break; } }
效果
4.支持以adapter的方式使用,也支持直接布局
apdater方式
布局文件:
<com.zhuguohui.learn.StackLayout android:id="@+id/gallery" app:offsetY="-20dp" app:offsetX="-20dp" app:offseetScale="5" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_centerInParent="true"> </com.zhuguohui.learn.StackLayout>
代码:
自定义的Adpater继承自StackLayout.BaseAdapter。
/** * Created by zhuguohui on 2016/4/26. */public class ImageAdapter extends StackLayout.BaseAdapter{ private int[] images; // 数据源 private Context context; public ImageAdapter(Context context,int[] images) { super(); this.context = context; this.images = images; } //设置显示的数量 @Override public int getVisibleCount() { return 3; } @Override public int getCount() { return images.length; } @Override public View getView(View view, int position, StackLayout parent) { ImageView imageView; if(view==null) { imageView = new ImageView(context); imageView.setScaleType(ImageView.ScaleType.FIT_XY); imageView.setLayoutParams(new Gallery.LayoutParams(500, 400)); }else { imageView= (ImageView) view; } Glide.with(context).load(images[position]).into(imageView); return imageView; }}
设置给StackLayout
gallery= (StackLayout) findViewById(R.id.gallery); adapter=new ImageAdapter(this,rid); gallery.setAdapter(adapter);
效果
直接布局方式
<com.zhuguohui.learn.StackLayout android:id="@+id/gallery" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_centerInParent="true" app:offseetScale="5" app:offsetX="-20dp" app:offsetY="-20dp"> <ImageView android:layout_width="300dp" android:layout_height="200dp" android:src="@drawable/image1" /> <ImageView android:layout_width="300dp" android:layout_height="200dp" android:src="@drawable/image2" /> <ImageView android:layout_width="300dp" android:layout_height="200dp" android:src="@drawable/image3" /> <ImageView android:layout_width="300dp" android:layout_height="200dp" android:src="@drawable/image4" /> </com.zhuguohui.learn.StackLayout>
效果就是最开始演示的效果,我就不放图了。
二.功能实现
关于功能实现方面的内容比较多,我就讲一些主要的东西,比如布局,view复用,观察者模式的使用。
1.布局
这个控件直接继承自ViewGroup,也就是说必须重写onLayout方法。关于View的叠放,我的思路是从后向前遍历view,通过设置TranslationY,TranslantionX,ScaleX,ScaleY来实现叠放的效果。同时记录View的起始中心点,这个主要是在后来拖动动画时需要。
@Override protected void onLayout(boolean changed, int l, int t, int r, int b) { //从中心布局 int mWidth = getWidth(); int mHight = getHeight(); int left = l + mWidth / 2; int top = t + mHight / 2; int childcount = getChildCount(); mViewPositionList.clear(); for (int i = 0; i < childcount; i++) { mViewPositionList.add(new Point(0, 0)); } //注意这里的循环遍历有两个,一个用来获取view,一个用来计算偏移量 for (int i = childcount - 1, j = 0; i >= 0; i--, j++) { View childView = getChildAt(i); int childTop = top - childView.getMeasuredHeight() / 2; int childLeft = left - childView.getMeasuredWidth() / 2; int childbuttom = childTop + childView.getMeasuredHeight(); int childright = childLeft + childView.getMeasuredWidth(); childView.layout(childLeft, childTop, childright, childbuttom); childView.setTranslationY(j * mOffsetY); childView.setTranslationX(j*mOffsetX); childView.setScaleX((1 - j * mOffsetScale)); childView.setScaleY((1 - j * mOffsetScale)); //记录view的起始中心点 Point point = mViewPositionList.get(i); point.set(childLeft + childView.getMeasuredWidth() / 2, childTop + childView.getMeasuredHeight() / 2); } }
2.伴随动画
在view被拖动的时候,计算新的位置与初始位置的偏移量,并除以View宽度的二分之一得到比率。此处使用offsetTopAndBottom,和offsetLeftAndRight方法修改view的位置,不会引起view的重绘。当然这个方法会在手指移动的时候调用。
private void updateView(int dx, int dy, int xMove, int yMove) { if (mSelectIndex != -1) { View view = getChildAt(mSelectIndex); if (view == null) { return; } Point point = mViewPositionList.get(mSelectIndex); //偏移view实现拖动效果 view.offsetTopAndBottom(dy); view.offsetLeftAndRight(dx); //计算新的中心的 int centerx = view.getLeft() + view.getWidth() / 2; int centery = view.getTop() + view.getHeight() / 2; //计算偏移量 int x = centerx - point.x; int y = centery - point.y; int distance = (int) Math.sqrt(x * x + y * y); // 计算比率 float rate = (float) (distance * 2.0 / view.getWidth()); //更新其他view updateViews(rate); } }
更新其他view,发现这个函数名没取好
private void updateViews(float rate) { if (rate > 1) { rate = 1; } int count = getChildCount(); int j = 1; //注意此处是从count-2开始循环,因为count-1为我们正在拖动的那个view for (int i = count - 2; i >= 0; i--, j++) { View view = getChildAt(i); float scaleX = (float) (1 - mOffsetScale * j); //计算新的缩放值,算法与onlayout中的一样,只是多了一点。 float newScale = (float) (scaleX + mOffsetScale * rate); view.setScaleY(newScale); view.setScaleX(newScale); float translateY = (j - rate) * mOffsetY; float translateX = (j - rate) * mOffsetX; view.setTranslationY(translateY); view.setTranslationX(translateX); } }
3.自动复位或飞出
此处的思路是计算X轴与Y轴的速度,如果速度大于一定值就飞出否则复位,在飞出的时候,根据速度的方向计算终点的X,Y坐标,并根据位移计算出时间,取最小的时间作为动画的时间,然后根据这些信息创建属性动画,在动画结束时添加复用view的处理。
private void autoDismissOrRestore(float velocityX, float velocityY) { if (mSelectIndex != -1) { boolean out = true; final View outView = getChildAt(mSelectIndex); int finalx = -1; int finaly = -1; int useTime = 0; int initLeft = 0; int initTop = 0; if (velocityX != 0 || velocityY != 0) { if (velocityX > 0) { finalx = getWidth(); } else { finalx = -getWidth(); } if (velocityY < 0) { finaly = -getHeight(); } else { finaly = getHeight(); } //计算移动距离 int distanceX = Math.abs(outView.getLeft() - finalx); int distanceY = Math.abs(outView.getRight() - finaly); int xTime = Integer.MAX_VALUE; int yTime = Integer.MAX_VALUE; if (velocityX != 0) { xTime = (int) (distanceX * 1000 / Math.abs(velocityX)); } else { yTime = (int) (distanceY * 1000 / Math.abs(velocityY)); } //计算时间 useTime = (int) Math.min(xTime, yTime); } else { //返回 Point point = mViewPositionList.get(mSelectIndex); initLeft = point.x - outView.getWidth() / 2; initTop = point.y - outView.getHeight() / 2; finalx = initLeft - outView.getLeft(); finaly = initTop - outView.getTop(); useTime = 200; out = false; } if (finalx != -1 || finaly != -1) { final boolean finalOut = out; ValueAnimator animatorX = ObjectAnimator.ofInt(finalx).setDuration(Math.abs(useTime)); animatorX.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { int lastOffset = 0; @Override public void onAnimationUpdate(ValueAnimator animation) { //此处使用的是offsetLeftAndRight,因为之前使用过setTranslationX发现 //view的显示范围与通过view.getlerf()的范围不一致。造成了,手指触摸是获取 //被触摸到的view不正确,因此才改用offsetLeftAndRight //还需要注意使用offsetLeftAndRight的时候设置的是偏移量,具有叠加的效果 //所以此处不能直接使用animation.getAnimatedValue() int offset = (int) animation.getAnimatedValue() - lastOffset; outView.offsetLeftAndRight(offset); lastOffset = (int) animation.getAnimatedValue(); } }); ValueAnimator animatorY = ObjectAnimator.ofInt(finaly).setDuration(Math.abs(useTime)); animatorY.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { int lastOffset = 0; @Override public void onAnimationUpdate(ValueAnimator animation) { int offset = (int) animation.getAnimatedValue() - lastOffset; outView.offsetTopAndBottom(offset); lastOffset = (int) animation.getAnimatedValue(); } }); animatorY.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { if (finalOut) { //复用view reuseView(outView); } else { //复位的时候,其他的view位置也要更新,手动设置比率为0就行了 updateViews(0); } mSelectIndex = -1; } }); AnimatorSet set = new AnimatorSet(); set.playTogether(animatorX, animatorY); set.start(); } } }
4.复用View
复用View,通过的是在view移除界面的时候不是调用removeView,而是调用removeViewInLayout,然后根据需要填充的数据,将移除的view更新数据后调用addViewInLayout,添加到view中,注意添加的位置是最后一个
private void reuseView(View outView) { //将移除的view插入到第一个,因为我们的layout是从最后开始显示的,所以第一个显示在最底层 //此处不需要使用removeView和addView因为这两个方法会 调用 requestLayout()和invalidate(true); removeViewInLayout(outView); //此处需要判断mAdapter是否为空,以防在不使用Adapter的情况下,也能正常显示 if (mAdapter!=null&&mNextPosition <= mAdapter.getCount() - 1) { View view=mAdapter.getView(outView,mNextPosition, StackLayout.this); addViewInLayout(view, 0, view.getLayoutParams(), true); mNextPosition++; } requestLayout(); }
5.观察者模式
观察者模式是对象的行为模式,又叫发布-订阅(Publish/Subscribe)模式、模型-视图(Model/View)模式、源-监听器(Source/Listener)模式或从属者(Dependents)模式。观察者模式定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象。这个主题对象在状态上发生变化时,会通知所有观察者对象,使它们能够自动更新自己。
UML图如下:
以上的内容是我从其他的地方copy过来的,简单一点,此处我们的观察者是StackLayout,被观察者是Adapter,当adapter有数据更新的时候,就吼一声,老子变了!,然后注册在Adapter中的StackLayout就开始刷新自己,你变我也变。由于这种设计模式很常用,系统以及给我们实现好了,要实现观察者只需要实现Observer接口,要实现被观察者可以有两种方式,一种是直接继承自Observable,或者内部有一个Observable的成员变量。
注册的时候调用addObserver
addObserver(observer);
移除的时候使用
deleteObserver(observer);
需要更新的时候使用setChanged和notifyObservers,
setChanged(); notifyObservers();
切记一定要先调用setChanged()因为在notifyObservers的时候会坚持是否发生改变
但是坑爹的是setChanged是一个受保护的方法
所以不能直接调用,除非你继承它,或者持有它的一个子类,系统的BaseAdapter 就是如此。
而我的Adapter没有那么多业务需求所以直接继承
public static abstract class BaseAdapter extends Observable { /** * 获取可见项的数量 * * @return */ public abstract int getVisibleCount(); /** * 获取数据大小 * * @return */ public abstract int getCount(); /** * 获取用于显示的view * * @param convertView 需要复用的view,如果第一次创建则为null * @param position 显示的位置 * @param parent 父view * @return */ public abstract View getView(View convertView, int position, StackLayout parent); /** * 发送更新 */ public void notifyDataSetChange() { setChanged(); notifyObservers(); } public void registerObserver(Observer observer) { addObserver(observer); } public void unRegisterObserver(Observer observer) { deleteObserver(observer); } }
设置Adapter
public void setAdapter(BaseAdapter adapter) { if (mAdapter != null) { mAdapter.unRegisterObserver(this); } if (adapter == null) { throw new IllegalArgumentException("adapter is null"); } mAdapter = adapter; //设置可见数量,可见数量不能比数据多 mVisibleSize = mAdapter.getVisibleCount() > mAdapter.getCount() ? mAdapter.getCount() : mAdapter.getVisibleCount(); adapter.registerObserver(this); adapter.notifyDataSetChange(); }
当adapter调用notifyDataSetChange的时候,被触发每一个注册在adapter中的Obserse的update的方法。
而我们的update的方法就是重置view
@Override public void update(Observable observable, Object data) { resetView(); } /** * 重置状态 */ private void resetView() { //清除以后的view removeAllViews(); //根据需要显示的数目创建view for (int i = 0; i < mVisibleSize; i++) { View view=mAdapter.getView(null,i,this); if(view!=null){ //注意此处的添加顺序 addView(view,0); mNextPosition++; } } }
6.自动飞出
很简单,模拟一个速度就行了
/** * 自动飞出 * @param left true表示从左边飞出,否则从右边 * @param up 如果为true表示从上边放飞出,否则从下边飞出 */ public void takeOff(boolean left,boolean up){ if(getChildCount()!=0){ mSelectIndex=getChildCount()-1; autoDismissOrRestore(left?-2000:2000,up?-2000:2000); } }
三.源码下载
https://github.com/zhuguohui/StackLayout
四.总结
通过这个自定义控件的编写,算是理清了很多东西,学会了使用观察者模式,复用view等技术。对我的成长还是蛮大的。以后会多试着写一下好用的控件。这是这个月最后一篇博客,希望大家不要觉得太水,如果你点赞我就更高兴了O(∩_∩)O~~
- 自定义层叠布局StackLayout
- StackLayout布局
- SWT:StackLayout(堆栈式布局)
- 自定义UICollectionViewLayout(二) ----StackLayout&CircleLayout
- stackLayout
- SWT(JFace)体验之StackLayout布局
- 自定义viewGroup+ViewDragHelper: 仿探探主页卡片式滑动,层叠布局
- Windows Phone canvas层叠布局
- Xamarin.Forms 用户界面——控件——布局——StackLayout
- 使用Relalayout实现层叠式布局
- wap简单的卡片层叠布局 滑动
- 层叠
- 自定义 ViewGroup 实现子 View 层叠效果
- StackLayout的使用
- 自定义Toast,防止层叠显示问题,和自定义Toast样式
- 自定义布局
- 自定义布局
- 自定义布局
- 设计模式笔记:观察者模式
- Android M(6.0) 权限解决方案
- 欢迎使用CSDN-markdown编辑器
- bzoj3223 文艺平衡树 Splay & Treap
- nginx支持tcp代理mysql
- 自定义层叠布局StackLayout
- Java批量文件打包下载
- 以小米4手机为例换算px,dip,dpi等数值
- Adapter不调用getView()的可能
- 合理使用Android提供的Annotation来提高代码的质量
- MySql安装过程以及中文乱码解决办法
- Effective-OC 10.在既有类中使用关联对象存储自定义数据
- Mysql字符串字段判断是否包含某个字符串的3种方法
- 浅谈httpclient