自定义层叠布局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~~

0 0
原创粉丝点击