自定义层叠布局StackLayout

一.效果

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~~

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值