android开发之仿QQ拖拽界面效果(侧滑面板)

仿QQ拖拽界面效果(侧滑面板),我们一般继承Layout,不会直接去继承ViewGroup,而是继承FrameLayout,为什么五大布局我们偏偏只继承FrameLayout呢?

  • 第一,FrameLayout继承ViewPager;

  • 第二,其他四大布局比FrameLayout多做了onDraw,onLayout,FrameLayout只有层级上下关系,没有位置的相对关系,而我们自定义控制对位置的相对关系是自定义的,不需要父类一开始就给我们定好,只需要父类给咱们测量控件的宽高即可。

如此这般,FrameLayout是最好的选择!

具体实现:

  • 创建一个类继承FrameLayout类,覆写其构造函数。
public DragLayout(Context context) {
    this(context, null);
}

public DragLayout(Context context, AttributeSet attrs) {
    this(context, attrs, 0);
}

public DragLayout(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
}
  • 初始化操作(通过静态方法),初始化ViewDragHelper对象:Google2013年IO大会提出的,解决界面控件拖拽移动问题。(v4包下)
/** * ViewGroup forParent:所要拖拽孩子的父View * float sensitivity:敏感度 * Callback cb:回调接口,当你触摸到子View的时候就会响应 * mTouchSlop:最小敏感范围,值越小越敏感 * public static ViewDragHelper create(ViewGroup forParent, float sensitivity, ViewDragHelper.Callback cb) { ViewDragHelper helper = create(forParent, cb); helper.mTouchSlop = (int)((float)helper.mTouchSlop * (1.0F / sensitivity)); return helper; } */

ViewDragHelper mDragHelper = ViewDragHelper.create(this, mCallback);
  • 传递触摸事件
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    //传递给mDragHelper
    return mDragHelper.shouldInterceptTouchEvent(ev);
}

@Override
public boolean onTouchEvent(MotionEvent event) {
    try {
        mDragHelper.processTouchEvent(event);
    }catch (Exception e) {
    }
    //返回true,持续接收事件
    return true;
}
  • 重写拖拽回调方法
ViewDragHelper.Callback mCallback =  new ViewDragHelper.Callback() {
    /* 1.根据返回结果决定当前child是否可以拖拽 child:当前被拖拽的View pointerId:区分多点触摸的id */
    @Override
    public boolean tryCaptureView(View child, int pointerId) {
        //直接return true,说明布局中的mLeftContent和mMainContent俩界面都能拖拽
        return child == mMainContent;
    }

    /* 2.根据建议值修正将要移动到的(横向)位置 */
    @Override
    public int clampViewPositionHorizontal(View child, int left, int dx) {
        return left;
    }
};
  • 获得布局中的子View
/** * 当xml填充结束之后,此方法被调用,同时它的所有的孩子都添加进来了 */
@Override
protected void onFinishInflate() {
    super.onFinishInflate();

    //Github
    //写注释
    //容错性检查(至少有俩子View,子View必须是ViewGroup的子类)
    if(getChildCount() < 2) {
        throw new IllegalStateException("Your ViewGroup must have two children at least!");
    }

    if(!(getChildAt(0) instanceof ViewGroup && getChildAt(1) instanceof ViewGroup)) {
        throw new IllegalArgumentException("Your children must be an instance of ViewGroup!");
    }

    mLeftContent = (ViewGroup) getChildAt(0);//根据索引找孩子
    mMainContent = (ViewGroup) getChildAt(1);//根据索引找孩子
}
  • 布局文件
<com.zanelove.androidcustomdemo.drag.DragLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
        android:background="@drawable/bg">

    <LinearLayout
        android:background="#66ff0000"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"/>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="#6600ff00"
        android:orientation="horizontal"
        />

</com.zanelove.androidcustomdemo.drag.DragLayout>
  • 效果图

以上,只是简单地演示一下效果!接下来,才是真正的开始!

  • 详解拖拽回调事件中的方法
ViewDragHelper.Callback mCallback =  new ViewDragHelper.Callback() {
    /** * 1.根据返回结果决定当前child是否可以拖拽 child:当前被拖拽的View pointerId:区分多点触摸的id * @param child * @param pointerId * @return */
    @Override
    public boolean tryCaptureView(View child, int pointerId) {
        Log.e(TAG,"tryCaptureView"+child);
        return child == mMainContent;
    }

    /** * 当capturedChild被捕获时,回调此方法 * @param capturedChild * @param activePointerId */
    @Override
    public void onViewCaptured(View capturedChild, int activePointerId) {
        Log.e(TAG,"onViewCaptured"+capturedChild);
        super.onViewCaptured(capturedChild, activePointerId);
    }

    @Override
    public int getViewHorizontalDragRange(View child) {
        return super.getViewHorizontalDragRange(child);
    }

    /** * 2.根据建议值修正将要移动到的(横向)位置 * @param child * @param left * @param dx * @return */
    @Override
    public int clampViewPositionHorizontal(View child, int left, int dx) {
        return left;
    }

    @Override
    public int clampViewPositionVertical(View child, int top, int dy) {
        return super.clampViewPositionVertical(child, top, dy);
    }

    @Override
    public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
        super.onViewPositionChanged(changedView, left, top, dx, dy);
    }

    @Override
    public void onViewReleased(View releasedChild, float xvel, float yvel) {
        super.onViewReleased(releasedChild, xvel, yvel);
    }
};

代码跟上来的朋友也行已经发现了,限制咱们的界面可以无限制的拖拽着,那么,我只想让红色界面只能拖拽整个屏幕的60%,那这个问题,我们该如何解决呢?在这个问题之前,我们还有一个问题,咱们的屏幕的宽度如何拿到呢?

问题一:咱们的屏幕的宽高度如何拿到呢?屏幕的60%如何获得呢?
解决方法:
我们第一反应就是这个测量的宽高度问题,应该放在onMeasure()方法中调用getMeasureWidth() or getMeasureHeight()方法来进行解决,那么,现在我们其实可以重写onSizeChanged()方法来解决获得宽高度的问题!

这个onSizeChanged()方法,也是在onMeasure()方法之后,但何时会被调用,取决于onMeasure()测量前后发现尺寸发生变化之后,才会去调用此方法。

/** * 当onMeasure方法前后测量尺寸有变化的时候回调此方法 * @param w * @param h * @param oldw * @param oldh */
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
   super.onSizeChanged(w, h, oldw, oldh);

   measureHeight = getMeasuredHeight(); //获得屏幕的高度
   measureWidth = getMeasuredWidth(); //获得屏幕的宽度
   mRange = (int) (measureWidth * 0.6f); //获得屏幕的60%
}

问题二:拿到了值之后我们只想让红色界面只能拖拽整个屏幕的60%?
解决方法:

/** * 2.根据建议值修正将要移动到的(横向)位置 * @param child 当前拖拽的View * @param left 新的位置的建议值 left = oldLeft = dx; * @param dx 位置变化量 * @return */
@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
    Log.e(TAG,"clampViewPositionHorizontal:"+" oldLeft"+child.getLeft() + " dx:"+dx+" left"+left);

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

/** * 根据范围修正左边的值 * @param left * @return */
private int fixLeft(int left) {
    if(left < 0) {
        return 0;
    }else if(left > mRange) {
        return mRange;
    }
    return left;
}

以上问题解决了,还有以下问题:拖拽左界面(左边红色区域)如同拖拽主界面(绿色区域),意思就是拖拽左界面产生的值以及所有的事件全部交给主界面来处理!

/** * 3.当View位置改变的时候,处理要做的事情(更新状态,伴随动画,重绘界面),注意此时的View已经发生了位置的改变 * @param changedView 改变位置的View * @param left 新的左边值 * @param top * @param dx 水平方向变化量 * @param dy */
@Override
public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
    super.onViewPositionChanged(changedView, left, top, dx, dy);

    int newLeft = left;
    if(changedView == mLeftContent) {
        //把当前变化量传递给mMainContent
        newLeft = mMainContent.getLeft() + dx;
        //进行修正
        newLeft = fixLeft(newLeft);
        //当左面板移动之后,再强制放回去
        mLeftContent.layout(0,0,0+measureWidth,0+measureHeight);
        mMainContent.layout(newLeft, 0, newLeft + measureWidth, 0 + measureHeight);
    }

    //为了兼容低版本,每次修改之后重绘界面
    invalidate();
}
  • 效果图:

到目前为止,拖拽先说到这里,那么接下来我们来说说动画,当我们拖拽界面到一定的位置的时候,我放手,那么被拖拽的界面就得找‘就近原则’了,然后使用动画回到应该到的位置!

那这个‘就近原则’如何确定?

  • 代码:
    关闭动画(主界面往左走):
/** * 当View被释放的时候,处理的事情(执行动画) * @param releasedChild 被释放的子View * @param xvel 水平方向的速度(右拖拽为正,左拖拽为负) * @param yvel 垂直方向的速度(上拖拽为负,下拖拽为正) * */
@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
    Log.d(TAG,"onViewReleased:"+" xvel"+xvel+" yvel:" + yvel);
    super.onViewReleased(releasedChild, xvel, yvel);
    //判断执行 关闭/开启
    //先考虑所有开启的情况,剩下的就都是关闭的情况
    if(xvel == 0 && mMainContent.getLeft() > mRange / 2.0f){
        open();
    }else if(xvel > 0) {
        open();
    }else{
        close();
    }
}

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

/** * 关闭时是否平滑 * @param isSmooth true 平滑 * false 不平滑 */
private void close(boolean isSmooth) {
    int finalLeft = 0;
    if(isSmooth) {
        //1.触发一个平滑动画
        if(mDragHelper.smoothSlideViewTo(mMainContent,finalLeft,0)){
            //返回true代表还没有移动到指定位置,需要刷新界面
            ViewCompat.postInvalidateOnAnimation(this); //参数传this(child所在的ViewGroup)
        }
    }else {
        mMainContent.layout(finalLeft, 0, finalLeft + measureWidth, 0 + measureHeight);
    }
}

@Override
public void computeScroll() {
     super.computeScroll();
     //2.持续平滑动画(高频率调用)
     if(mDragHelper.continueSettling(true)) {
         //如果返回true,还需要继续执行
         ViewCompat.postInvalidateOnAnimation(this); //参数传this(child所在的ViewGroup)
     }
 }

开启动画(主界面往右走):

/** * 开启 */
private void open() {
    open(true);
}

/** * 开启时是否平滑 * @param isSmooth true 平滑 * false 不平滑 */
private void open(boolean isSmooth) {
    int finalLeft = mRange;
    if(isSmooth) {
        //1.触发一个平滑动画
        if(mDragHelper.smoothSlideViewTo(mMainContent,finalLeft,0)){
            //返回true代表还没有移动到指定位置,需要刷新界面
            ViewCompat.postInvalidateOnAnimation(this); //参数传this(child所在的ViewGroup)
        }
    }else {
        mMainContent.layout(finalLeft,0,finalLeft + measureWidth,0 + measureHeight);
    }
}

效果图,我就不贴了!还是你自己运行在自己的真机上或者模拟器上体验体验吧!

好了,到目前位置,大概的一个框架我们搭建好了,也许好多人会问我,说好的仿QQ界面的呢?咋就给我整个了这逼玩意儿!

那好,现在就给大伙分析分析QQ界面有哪些是我们要效仿的:

  • 伴随动画:
    1. 左面板:缩放动画,平移动画,透明度动画
    2. 主界面:缩放动画
    3. 背景动画:亮度变化(颜色变化)
/** * 3.当View位置改变的时候,处理要做的事情(更新状态,伴随动画,重绘界面),注意此时的View已经发生了位置的改变 * @param changedView 改变位置的View * @param left 新的左边值 * @param top * @param dx 水平方向变化量 (右拖拽为正,左拖拽为负) * @param dy */
@Override
public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
    super.onViewPositionChanged(changedView, left, top, dx, dy);

    int newLeft = left;
    if(changedView == mLeftContent) {
        //把当前变化量传递给mMainContent
        newLeft = mMainContent.getLeft() + dx;
        //进行修正
        newLeft = fixLeft(newLeft);
        //当左面板移动之后,再强制放回去
        mLeftContent.layout(0,0,0+measureWidth,0+measureHeight);
        mMainContent.layout(newLeft, 0, newLeft + measureWidth, 0 + measureHeight);
    }

    //更新状态,执行动画
    dispatchDragEvent(newLeft);

    //为了兼容低版本,每次修改之后重绘界面
    invalidate();
}

private void dispatchDragEvent(int newLeft) {
    /** * Github * Jake Wharton * nineoldandroids.jar 属性动画兼容低版本 * ActionBarSherlock 状态栏 */
    float percent = newLeft * 1.0f / mRange;
// 伴随动画:
// 1. 左面板:缩放动画,平移动画,透明度动画
    /* 3.0以上版本才兼容 mLeftContent.setScaleX(0.5f + 0.5f * percent); mLeftContent.setScaleY(0.5f + 0.5f * percent); */
    //需要导入nineoldandroids.jar包
    /** * 缩放动画 */
    ViewHelper.setScaleX(mLeftContent,evaluate(percent,0.5f,1.0f));
    ViewHelper.setScaleY(mLeftContent,0.5f + 0.5f * percent);
    /** * 平移动画 -mWidth / 2.0f -> 0.0f */
    ViewHelper.setTranslationX(mLeftContent,evaluate(percent,-measureWidth / 2.0f,0));
    /** * 透明度 */
    ViewHelper.setAlpha(mLeftContent,evaluate(percent,0.5f,1.0f));
// 2. 主界面:缩放动画
    ViewHelper.setScaleX(mMainContent,evaluate(percent,1.0f,0.8f));
    ViewHelper.setScaleY(mMainContent,evaluate(percent,1.0f,0.8f));
// 3. 背景动画:亮度变化(颜色变化)
    getBackground().setColorFilter((Integer)evaluateColor(percent, Color.BLACK,Color.TRANSPARENT),PorterDuff.Mode.SRC_OVER);
}

public Float evaluate(float fraction,Number startValue,Number endValue){
    float startFloat = startValue.floatValue();
    return startFloat + fraction * (endValue.floatValue() - startFloat);
}

/** * 颜色变化过度 * @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))));
}
  • 状态监听
private OnDragStatusChangeListener mListener;
//初始状态
private Status mStatus = Status.Close;
/** * 状态枚举 */
public static enum Status{
    Close,Open,Draging;
}

public interface OnDragStatusChangeListener {
    void onClose();
    void onOpen();
    void onDraging(float percent);
}

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

private void dispatchDragEvent(int newLeft) {
    float percent = newLeft * 1.0f / mRange;

    //更新状态,执行回调
    Status perStatus = mStatus; //上一次状态
    mStatus = updateStatus(percent);
    if(mStatus != perStatus) {
        //状态发生变化
        if(mStatus == Status.Close) {
            //当前变为关闭状态
            if(mListener != null) {
                mListener.onClose();
            }
        }else if(mStatus == Status.Open) {
            if(mListener != null) {
                mListener.onOpen();
            }
        }
    }

    //每时每刻都在调用onDraging()
    if(mListener != null) {
        mListener.onDraging(percent);
    }

    /** * 伴随动画: */
    animViews(percent);
}

在MainActivity类中:

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    final ListView mLeftList = (ListView) findViewById(R.id.lv_left);
    ListView mMainList = (ListView) findViewById(R.id.lv_main);
    final ImageView iv_head = (ImageView) findViewById(R.id.iv_head);

    //查找DragLayout,设置监听
    DragLayout mDragLayout = (DragLayout) findViewById(R.id.dl);
    mDragLayout.setDragStatusListener(new DragLayout.OnDragStatusChangeListener() {
        @Override
        public void onClose() {
            Util.showToast(MainActivity.this,"onClose");
            //让图标晃动
            ObjectAnimator mAnim = ObjectAnimator.ofFloat(iv_head, "translationX", 15.0f);
            mAnim.setInterpolator(new CycleInterpolator(4));//差值器 来回晃动4圈
            mAnim.setDuration(800);
            mAnim.start();
        }

        @Override
        public void onOpen() {
            Util.showToast(MainActivity.this,"onOpen");

            //验证回调方法:左面板ListView随机设置一个条目
            Random random = new Random();
            int nextInt = random.nextInt(50);
            mLeftList.smoothScrollToPosition(nextInt);
        }

        @Override
        public void onDraging(float percent) {
            //更新图标的透明度
            //1.0 -> 0.0
            ViewHelper.setAlpha(iv_head,1 - percent);
        }
    });

    mLeftList.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 mTextView = (TextView)view;
            mTextView.setTextColor(Color.WHITE);
            return view;
        }
    });
    mMainList.setAdapter(new ArrayAdapter<String>(this,android.R.layout.simple_list_item_1, Cheeses.NAMES){
        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
            View view = super.getView(position, convertView, parent);
            TextView mTextView = (TextView)view;
            mTextView.setTextColor(Color.BLACK);
            return view;
        }
    });
}

效果图:

细心的朋友也行看到了,现在的界面布局都不一样了,哈哈!关于布局我就不一一粘贴复制了,你们等会直接下载我的Demo看看就OK了。

为了做到高仿QQ拖拽界面效果,那么现在就明摆着一个细节就是:被拖拽的主界面的ListView是不能滚动的,而我们的却滚动了!这可如何是好呀!

  • 触摸优化,重写ViewGroup;当左界面处于Draging或Open状态时,主界面的ListView事件应该禁用!

    • 自定义LinearLayout
    • 重写OnInterceptionTouchEvent()和onTouchEvent()
public class DragLinearLayout extends LinearLayout {
    private DragLayout mDragLayout;
    public DragLinearLayout(Context context) {
        super(context);
    }

    public DragLinearLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public void setDragLayout(DragLayout mDragLayout){
        this.mDragLayout = mDragLayout;
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        //如果当前是关闭状态,子View:ListView能滚动
        if(mDragLayout.getStatus() == DragLayout.Status.Close) {
            return super.onInterceptTouchEvent(ev); //false,不拦截事件,由子View来处理
        }else {
            return true; //拦截事件,交给DragLinearLayout来处理
        }
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        //如果当前是关闭状态,子View:ListView能滚动
        if(mDragLayout.getStatus() == DragLayout.Status.Close) {
            return super.onTouchEvent(event); //false,不拦截事件,由子View来处理
        }else {
            //手指抬起,执行关闭操作
            if(event.getAction() == MotionEvent.ACTION_UP) {
                mDragLayout.close();
            }
            return true; //拦截事件,交给DragLinearLayout来处理
        }
    }
}

示例代码戳Here

你可能感兴趣的:(Android开发,自定义view,侧滑面板,仿QQ拖拽界面)