侧滑面板

自定义控件

package com.itheima57.draglayout.ui;

import com.nineoldandroids.view.ViewHelper;

import android.content.Context;
import android.graphics.Color;
import android.graphics.PorterDuff.Mode;
import android.support.v4.view.ViewCompat;
import android.support.v4.widget.ViewDragHelper;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewDebug;
import android.view.ViewGroup;
import android.widget.FrameLayout;

/** * 侧滑面板 * @author poplar * */
public class DragLayout extends FrameLayout {

    private ViewDragHelper mDragHelper;
    private ViewGroup mLeftContent; // 左面板
    private ViewGroup mMainContent; // 主面板
    private int mHeight;// 控件高度
    private int mWidth; //控件宽度
    private int mRange; // 横向拖拽范围

    public static enum Status {
        Close, Open, Draging
    }
    private Status status = Status.Close;
    private OnDragStatusChangeListener mListener;

    /** * 状态更新接口 * @author poplar * */
    public interface OnDragStatusChangeListener {
        void onOpen();
        void onClose();
        void onDraging(float percent);
    }

    public void setOnDragStatusChangeListener(OnDragStatusChangeListener mListener){
        this.mListener = mListener;
    }

    // 在代码里 new 对象
    public DragLayout(Context context) {
        this(context, null);
    }

    // 布局文件中使用, AttributeSet 布局文件中声明的属性
    public DragLayout(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }


    public DragLayout(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);

        // 1. 初始化拖拽辅助工具, 通过静态方法

        // forParent ViewDragHelper生效的布局
        // sensitivity 敏感度, 越大越敏感 1.0默认值
        // Callback 提供信息, 接受事件
        mDragHelper = ViewDragHelper.create(this, 1.0f, mCallback);

    }

    /** * 3. 重写 Callback父类方法, 接受并处理 ViewDragHelper 解析后的信息 */
    ViewDragHelper.Callback mCallback = new ViewDragHelper.Callback() {

        // a. 尝试捕获View, 决定了child是否可以被拖拽
        // 返回true, 表示child可以被拖拽
        @Override
        public boolean tryCaptureView(View child, int pointerId) {
            // child 被拖拽的子View / ViewGroup
            // pointerId 多点触摸的手指id
// System.out.println("child: " + child.toString());
            return true;
        }

        // 当capturedChild被捕获时, 调用
        @Override
        public void onViewCaptured(View capturedChild, int activePointerId) {
            super.onViewCaptured(capturedChild, activePointerId);
        }

        // b.设置视图横向的拖拽范围, 不限制真正的位置
        // 由返回值确定了 动画执行 时长, 以及此方向(水平)是否可以被拖拽
        // 大于0 才可以拖拽
        @Override
        public int getViewHorizontalDragRange(View child) {
            return mRange;
        }

        /** * c. (重要)决定了横向的滑动位置, left 是建议值, 在这里可以进行拖拽位置的修正 * 此时将要发生移动, 还未真正移动 */
        @Override
        public int clampViewPositionHorizontal(View child, int left, int dx) {
            // child 将要发生位移的View
            // left 将要移动到的位置, left = oldLeft + dx;
            // dx 是变化量
            int oldLeft = mMainContent.getLeft();
// System.out.println("clampViewPositionHorizontal: left: " + left + " oldLeft: " + oldLeft + " dx: " + dx);

            if(child == mMainContent){
                left = fixLeft(left);
            }

            // 返回值决定了真正要移动到的位置
            return left;
        }

        /** * d. (重要)决定了位置发生改变之后, 要做的事情. 伴随动画, 更新状态, 执行回调 */
        @Override
        public void onViewPositionChanged(View changedView, int left, int top,
                int dx, int dy) {
            // changedView : 位置发生改变的View
            // left : 最新的横向位置
            // top : 最新的纵向位置
            // dx : 刚刚发生的横向偏移量
            // dy : 刚刚发生的纵向偏移量
            System.out.println("onViewPositionChanged: left: " + left + " dx: " + dx);

            if(changedView == mLeftContent){
                // 发现拖拽的是左面板, 强制放回原位置
                mLeftContent.layout(0, 0, mWidth, mHeight);

                // 把变化量转交给主面板
                int newLeft = mMainContent.getLeft() + dx;

                // 再次修正
                newLeft = fixLeft(newLeft);

                mMainContent.layout(newLeft, 0, newLeft + mWidth, 0 + mHeight);
            }

            dispatchDragEvent();


            invalidate(); // 为了兼容低版本, 需要手动重回界面,使改变的值生效
        }


        /** * e. 当View被释放时调用此方法, 做动画. */
        @Override
        public void onViewReleased(View releasedChild, float xvel, float yvel) {
            // releasedChild 被释放的孩子
            // xvel : 水平方向的速度 向右+ 向左-
            // yvel : 垂直方向的速度 向下+ 下上-

            // 根据松手时的速度或位置执行动画
            if(xvel == 0 && mMainContent.getLeft() > mRange * 0.5f){
                // 静止松手, 左面板一半的右边, 打开
                open();
            } else if (xvel > 0) {
                open();
            } else {
                close();
            }

        }

        @Override
        public void onViewDragStateChanged(int state) {
            super.onViewDragStateChanged(state);
// * @see #STATE_IDLE // 限制状态 
// * @see #STATE_DRAGGING // 拖拽状态
// * @see #STATE_SETTLING // 自动化状态
        }


    };

    /** * 修正边界 * @param left * @return */
    private int fixLeft(int left) {
        if(left < 0){
            // 限定左边界
            return 0;
        } else if (left > mRange) {
            // 限定右边界
            return mRange;
        }

        return left;
    }

    /** * 分发拖拽事件, 伴随动画 */
    protected void dispatchDragEvent() {
        int left = mMainContent.getLeft();
        // 0.0 -> 1.0
        float percent = left * 1.0f / mRange;
        System.out.println("percent: " + percent);

        // 执行动画
        animViews(percent);

        // 更新状态, 执行回调

        if(mListener != null){
            mListener.onDraging(percent);
        }

        Status preStatus = status;
        status = updateStatus(percent);
        // 对比最新的状态和刚刚的状态
        if(preStatus != status && mListener != null){
            // 状态发生了改变
            if(status == Status.Close){
                mListener.onClose();
            }else if (status == Status.Open) {
                mListener.onOpen();
            }
        }

    }

    /** * 更新状态 * @param percent * @return */
    private Status updateStatus(float percent) {
        if(percent == 0f){
            return Status.Close;
        }else if (percent == 1.0f) {
            return Status.Open;
        }
        return Status.Draging;
    }

    private void animViews(float percent) {
        // - 左面板: 缩放动画, 透明度动画, 平移动画
                // 0.5 -> 1.0 >>> percent * 0.5f + 0.5f
        // mLeftContent.setScaleX(percent * 0.5f + 0.5f);
        // mLeftContent.setScaleY(percent * 0.5f + 0.5f);
        // float f = percent * 0.5f + 0.5f;
                ViewHelper.setScaleX(mLeftContent, evaluate(percent, 0.5f, 1.0f));
                ViewHelper.setScaleY(mLeftContent, evaluate(percent, 0.5f, 1.0f));

                // 0.5 -> 1.0 >>> 0.4 ->1.0
                ViewHelper.setAlpha(mLeftContent, evaluate(percent, 0.4f, 1.0f));

                // 平移动画 -mWidth*0.5f -> 0 // 类型估值器
                ViewHelper.setTranslationX(mLeftContent, evaluate(percent, -mWidth*0.5f, 0));

        // - 主面板: 缩放动画 // 1.0 -> 0.8
                ViewHelper.setScaleX(mMainContent, evaluate(percent, 1.0f, 0.8f));
                ViewHelper.setScaleY(mMainContent, evaluate(percent, 1.0f, 0.8f));

        // - 背景: 亮度
                getBackground().setColorFilter((Integer)evaluateColor(percent, Color.BLACK, Color.TRANSPARENT), Mode.SRC_OVER);
    }

    /** * 颜色估算器 * @param fraction * @param startValue * @param endValue * @return */
    public Object evaluateColor(float fraction, Object startValue, Object 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))));
    }

    /** * 估值器, 估算一个从开始到结束的中间值 * @param fraction 分度值 * @param startValue 开始值 * @param endValue 结束值 * @return */
    public Float evaluate(float fraction, Number startValue, Number endValue) {
        float startFloat = startValue.floatValue();
        return startFloat + fraction * (endValue.floatValue() - startFloat);
    }

    @Override
    public void computeScroll() {
        super.computeScroll();
        // b. 维持动画的继续
        if(mDragHelper.continueSettling(true)){
            // true说明动画还没移动到指定位置, 需要继续引发重绘
            ViewCompat.postInvalidateOnAnimation(this);
        }

    }

    /** * 关闭 */
    protected void close() {
        close(true);
    }

    public void close(boolean isSmooth){
        int finalLeft = 0;
        if(isSmooth){
            // 平滑动画
            // a. 触发平滑动画
            if(mDragHelper.smoothSlideViewTo(mMainContent, finalLeft, 0)){
                // 说明还没移动到最终位置, 需要重绘界面. 参数传控件所在ViewGroup
                ViewCompat.postInvalidateOnAnimation(this);
            }

        }else {
            mMainContent.layout(finalLeft, 0,finalLeft + mWidth, mHeight);
        }

    }

    /** * 打开 */
    protected void open() {
        open(true);
    }

    public void open(boolean isSmooth){

        int finalLeft = mRange;
        if(isSmooth){
            // 平滑动画
            // a. 触发平滑动画
            if(mDragHelper.smoothSlideViewTo(mMainContent, finalLeft, 0)){
                // 说明还没移动到最终位置, 需要重绘界面. 参数传控件所在ViewGroup
                ViewCompat.postInvalidateOnAnimation(this);
            }

        }else {
            mMainContent.layout(finalLeft, 0,finalLeft + mWidth, mHeight);
        }


    }

    /** * 2. 转交触摸事件拦截判断 */
    public boolean onInterceptTouchEvent(android.view.MotionEvent ev) {
        return mDragHelper.shouldInterceptTouchEvent(ev);
    };

    @Override
    public boolean onTouchEvent(MotionEvent event) {

        // 拦截后, 交由mDragHelper处理触摸事件
        try {
            mDragHelper.processTouchEvent(event);
        } catch (Exception e) {
            e.printStackTrace();
        }

        // 由当前控件消费事件
        return true;
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        // 当尺寸改变时调用.onMeasure之后调用.

        mHeight = mMainContent.getMeasuredHeight();
        mWidth = mMainContent.getMeasuredWidth();

        mRange = (int) (mWidth * 0.6f);

    }

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        // 当xml完全填充成view对象时,调用

        // Github 做一个健壮性检查
        // 检查孩子的个数
        if(getChildCount() < 2){
            throw new IllegalStateException("孩子必须至少有俩, Your layout must contains 2 children at least!");
        }
        // 检查孩子的类型, 必须是ViewGroup子类
        if(!(getChildAt(0) instanceof ViewGroup) || !(getChildAt(1) instanceof ViewGroup)) {
            throw new IllegalArgumentException("孩子必须是ViewGroup的子类, Your child must be an instance of ViewGroup");
        }

        // 获取左面板
        mLeftContent = (ViewGroup) getChildAt(0);

        // 获取主面板
        mMainContent = (ViewGroup) getChildAt(1);

    }

}

MainActivity:

package com.itheima57.draglayout;

import java.util.Random;

import android.app.Activity;
import android.graphics.Color;
import android.os.Bundle;
import android.view.View;
import android.view.ViewGroup;
import android.view.Window;
import android.view.animation.CycleInterpolator;
import android.widget.ArrayAdapter;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.TextView;

import com.itheima57.draglayout.ui.DragLayout;
import com.itheima57.draglayout.ui.DragLayout.OnDragStatusChangeListener;
import com.itheima57.draglayout.util.Cheeses;
import com.itheima57.draglayout.util.Utils;
import com.nineoldandroids.animation.ObjectAnimator;
import com.nineoldandroids.view.ViewHelper;

public class MainActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        requestWindowFeature(Window.FEATURE_NO_TITLE);
        setContentView(R.layout.activity_main);
        final ImageView iv_header = (ImageView) findViewById(R.id.iv_header);
        final ListView lv_left = (ListView) findViewById(R.id.lv_left);
        ListView lv_main = (ListView) findViewById(R.id.lv_main);

        DragLayout dragLayout = (DragLayout) findViewById(R.id.dl);

        dragLayout.setOnDragStatusChangeListener(new OnDragStatusChangeListener() {

            @Override
            public void onOpen() {
                // 打开, 左ListView随机跳动
                Utils.showToast(MainActivity.this, "onOpen");
                lv_left.smoothScrollToPosition(new Random().nextInt(50));
            }

            @Override
            public void onDraging(float percent) {
                // 动态更新小头像的透明度 1.0 -> 0.0
                Utils.showToast(MainActivity.this, "onDraging: "+ percent);
                ViewHelper.setAlpha(iv_header, 1 - percent);
            }

            @Override
            public void onClose() {
                // 小头像执行动画
                Utils.showToast(MainActivity.this, "onClose");

//              iv_header.setTranslationX(float translationX);

                ObjectAnimator mAnim = ObjectAnimator.ofFloat(iv_header, "translationX", 15f);
                mAnim.setInterpolator(new CycleInterpolator(4f));
                mAnim.setDuration(500);
                mAnim.start();

            }
        });


        lv_left.setAdapter(new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, Cheeses.sCheeseStrings){
            @Override
            public View getView(int position, View convertView, ViewGroup parent) {
                View view = super.getView(position, convertView, parent);
                ((TextView)view).setTextColor(Color.WHITE);
                return view;
            }
        });

        lv_main.setAdapter(new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, Cheeses.NAMES));

    }


}

你可能感兴趣的:(自定义控件)