• Android 开发实践 ViewGroup 实现左右滑出窗口(二)


    接上一篇

    <Android 开发实践 ViewGroup 实现左右滑出窗口(一)http://www.cnblogs.com/inkheart0124/p/3532862.html>

     源码: https://files.cnblogs.com/inkheart0124/PushSlider.zip

    ViewGroup中touch事件的处理,有三个函数

    public boolean dispatchTouchEvent(MotionEvent ev);事件派发

    public boolean onInterceptTouchEvent(MotionEvent ev);事件拦截

    public boolean onTouchEvent(MotionEvent ev);事件处理

    (废话一句,View中只有两个,dispatchTouchEvent和onTouchEvent,ViewGroup继承了View以后增加了onInterceptTouchEvent)

    一次完整的touch事件:ACTION_DOWN->ACTION_MOVE->ACTION_MOVE->ACTION_MOVE....->ACTION_UP

    其中对ACTION_DOWN的处理返回值比较重要,如果dispatchTouchEvent时返回false,那表示这一次touch事件我都不需要,之后的move和up都不会再传给我的ViewGroup。

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev){
        int action = ev.getAction();
        boolean gdHandled = mGDetector.onTouchEvent(ev);
        if((gdHandled) && (action == MotionEvent.ACTION_UP)){
            log("[dispatchTouchEvent] action = " + action + "return true");
            return true;
        }
        log("[dispatchTouchEvent] action = " + action + "return false");
        return super.dispatchTouchEvent(ev);
    }
        
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev){
        int action = ev.getAction();
        boolean gdHandled = mGDetector.onTouchEvent(ev);
        if((gdHandled) && (action == MotionEvent.ACTION_UP)){
            log("[onInterceptTouchEvent] action = " + action + "return true");
            return true;
        }
        log("[onInterceptTouchEvent] action = " + action + "return false");
        return super.onInterceptTouchEvent(ev);
    }
        
    //@Override
    public boolean onTouchEvent(MotionEvent ev){
        int action = ev.getAction();
        return true;
    }

    重写这三个函数,先把event直接丢给mGDetector处理,然后在调用系统的派发机制把消息传递到子View。

    注意Down消息一定不能拦截,ViewGroup主要根据move和up的情况来区别手势,当收到down消息的时候,我们还不能确定用户是要滑动呢还是要操作子view中的控件。Down消息如果被拦截,意味着这一个消息组都不会再传给子view。

    ==========================================================

    这里我单独处理了ACTION_UP,当手势处理已经消费了此消息(即mGDetector.onTouchEvent(ev)返回true)时,不在继续派发,而是直接返回true。

    这是因为当用户点住屏幕慢慢拖动(scroll手势)到左边再拖回来,而点住的位置又恰好是一个可以onClick的控件时,当用户松开屏幕,up消息将导致onClick发生。

    实际上我后来取消了scroll手势,只处理fling手势。因为一个朋友体验后告诉我,没人会慢慢拖出侧边菜单,而当用户犹豫的在界面上拖来拖去的时候,多半是在犹豫要在主界面上干点什么,如果划着划着突然出来个菜单,打断了思路会觉得很不爽。我觉得他说的很有道理。

    ==========================================================

    onTouchEvent中什么都没做,直接返回了true,这个方法会在dispatchTouchEvent函数调用super.dispatchTouchEvent(ev)时跑到,它的返回值就是super.dispatchTouchEvent(ev)的返回值,这里至少在down消息的时候必须返回true,否则就像前面说的那样,整组消息都不会再传递给我的pushslider了。

    手势:

    PushSlider类实现了android.view.GestureDetector.OnGestureListener接口,在前一篇的构造函数中我们创建了一个手势对象,并注册监听。

    mGDetector = new GestureDetector(mContext,this);

    下面就要实现OnGestureListener中的方法

    public boolean onDown(MotionEvent ev);处理down消息

    public void onLongPress(MotionEvent arg0);长按

    public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY);拖动,收到move消息时会跑到

    public boolean onFling(MotionEvent eDown, MotionEvent eMove, float velocityX,float velocityY);快速滑抛

    public void onShowPress(MotionEvent arg0);发生down消息后,用来显示高亮等可视化的处理

    public boolean onSingleTapUp(MotionEvent ev);处理一次轻触

    详细的解释就不写了,android developer官网上解释的很详细。这里我只需要处理onFling和onSingleTapUp,onScroll的代码后来没用,屏蔽掉了。

    onFling 是发生donw-move-move....-up时,最后up事件时会调用到,根据入参float velocityX和float velocityY这两个速度值,决定这是不是我们想要的一次滑抛手势

     1 public boolean onFling(MotionEvent eDown, MotionEvent eMove,float velocityX,float velocityY) {
     2     log("onFling " + eDown.getX() + "," + eMove.getX() +"," + velocityX);
     3 
     4     //<<<
     5     if((eDown.getX() - eMove.getX() > FLING_DISTANCE*mDensity) && (Math.abs(velocityX)> FLING_VELOCITY)){
     6         if(isMoveAllow(MOVE_FLAG_ALLOW_LEFT)){
     7             flingTo(MOVE_FLAG_ALLOW_LEFT,false);
     8             log("[onFling] true");
     9             return true;
    10         }
    11     }
    12         
    13     //>>>
    14     if((eMove.getX() - eDown.getX() > FLING_DISTANCE*mDensity) && (Math.abs(velocityX)> FLING_VELOCITY)){
    15         if(isMoveAllow(MOVE_FLAG_ALLOW_RIGHT)){
    16             flingTo(MOVE_FLAG_ALLOW_RIGHT,false);
    17             log("[onFling] true");
    18             return true;
    19         }
    20     }
    21     log("[onFling] false");
    22     return false;
    23 }

    5~11行,手势是从右向左 <<<--

    14~20行,手势是从左向右 -->>>

    根据eMove和eDown的相对位移,及X方向的速度,判断是否滑到下一页。

    onSingleTapUp 是发生down-up这样的一次点击(轻触)时调用

     1 @Override 
     2 public boolean onSingleTapUp(MotionEvent ev) {
     3     Rect focusRect = new Rect();
     4     mChild[focusedIndex].getHitRect(focusRect);
     5     float x = ev.getX();
     6     int left = focusRect.left-mScroller.getCurrX();
     7     int right = focusRect.right-mScroller.getCurrX();
     8         
     9     if((x < left) && isMoveAllow(MOVE_FLAG_ALLOW_RIGHT)){
    10         flingTo(MOVE_FLAG_ALLOW_RIGHT,true);
    11         log("[onSingleTapUp] true");
    12         return true;
    13     }else if((x > right) && isMoveAllow(MOVE_FLAG_ALLOW_LEFT)){
    14         flingTo(MOVE_FLAG_ALLOW_LEFT,true);
    15         log("[onSingleTapUp] true");
    16         return true;
    17     }
    18     log("[onSingleTapUp] false");
    19     return false;
    20 }
    21 
    22     
    23 }

    3,4行,取得当前focus的子view的区域;

    6,7行,这里取到的HitRect是相对与viewgroup的0,0点的相对坐标。需要换算成实际坐标,mScroller.getCurrX()是viewgroup的x位置相对于屏幕原点的值。

    由于我有一个滑出子view是半屏的,当这个半屏的设置菜单被唤出后,我希望轻触一下菜单以外的区域 就可以关闭菜单回到主界面。重写onSingleTapUp方法就是为了处理这种情况。

    flingTo,关于实现滑动,其实有很多方法,最直观的就像我们在onLayout中做的那样,重新算好每个子View的位置,然后layout一遍。

    但我觉得用view自己的Scroller类是最简单最省心的。

     1 private void flingTo(int direct,boolean fast){
     2     if(mForbidden)
     3         return;
     4 
     5     int startX = getScrollX();        
     6     int endX=0;
     7     int dx;
     8 
     9     if(direct == MOVE_FLAG_ALLOW_LEFT){
    10         focusedIndex ++;
    11 
    12         if(focusedIndex == SLIDER_PAGE_MIDDLE)
    13             endX = 0;
    14         else if(focusedIndex == SLIDER_PAGE_RIGHT)
    15             endX = mRightPosX;
    16 
    17         dx = endX - startX;
    18         mScroller.startScroll(startX, 0, dx, 0,(fast?100:250));
    19     }else{
    20         focusedIndex --;
    21         if(focusedIndex == SLIDER_PAGE_MIDDLE)
    22             endX = 0;
    23         else if(focusedIndex == SLIDER_PAGE_LEFT)
    24             endX = mLeftPosX;
    25 
    26         dx = endX - startX;
    27         mScroller.startScroll(startX, 0, dx, 0,(fast?100:250));
    28     }
    29     log("flingTo " + startX);
    30     invalidate();
    31     changeFocus();
    32 }

    5行,startx,得到当前ViewGroup的位置(相对于屏幕原点);

    10~15行,切换焦点,根据新的焦点得到endX,及滑动后ViewGroup的位置;

    18行,直接调用Scroller的startScroll方法,就开始滑动了,很简单吧!

    startScroll原型:public void startScroll(int startX, int startY, int dx, int dy, int duration)

    5个入参意义一目了然。

    flingTo函数的两个入参 direct是滑动方向,还有一个boolean fast,表示滑动速度,用它来确定startScroll的duration,100和250是实测的经验值。

    当滑抛手势唤出或关闭半屏菜单时,fast=false。让用户看到一个清晰的滑动过程。

    当用户按menu或cancel键的时候,fast=true。快速唤出或关闭界面。

    menu键和canel键的处理,我没有把它们定义在PushSlider的管理范畴内。这两个键值仍然是有activity处理,PushSlider只需要给出一个public的方法。

    简单的说,PushSlider就管自己的3个子View,他们的显示,位置,滑动,焦点。具体功能上的东东全都是activity的活。

    因此PushSlider除了让activity监听焦点变化外(见上一篇),还需要提供一些查询和操作接口,比如查询当前焦点,查询当前是否处于滑动中。

    比较重要的操作有两个:

    public void pushForbid(boolean forbid){
      mForbidden = forbid;
     }

    某些情况下,activity可能想不让用户滑动切换焦点,就可以pushForbid。

    另一个public void moveToPageById(int index),activity可直达某个子View,比如前面提到的 按menu键唤出设置菜单的子View。

    具体代码不贴了,仍然是调用flingTo(fast)的模式。

    PushSlider搞定了,看一下activity中怎么用它。

    layout/main.xml

     1 <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
     2     android:layout_height="fill_parent"
     3     android:layout_width="fill_parent"
     4     >
     5     
     6     <com.meflame.where.PushSlider android:id="@+id/push_view"
     7         android:layout_height="fill_parent"
     8         android:layout_width="fill_parent"
     9         >
    10             <!-- left page -->
    11             <include layout="@layout/menu" />
    12             <!-- middle page -->
    13             <include layout="@layout/record_list" />
    14             <!-- right page <include layout="@layout/map" />  -->
    15     </com.meflame.where.PushSlider>
    16     
    17     <ViewStub 
    18         android:id="@+id/waiting_stub"
    19         android:layout="@layout/waiting_view"
    20         android:layout_width="fill_parent"
    21         android:layout_height="fill_parent"
    22         />
    23 </RelativeLayout> 

    6~15行,加入PushSlider,插入三个子View,分别是

    <include layout="@layout/menu" />  左边菜单

    <include layout="@layout/record_list" /> 中间主界面列表

    <include layout="@layout/map" /> 右边最初设计是地图,后来没这么做,改成了一个独立的activity,因为地图本身就要获取各种各样的手势。

    这三个子View的layout按照一般的layout文件写就ok,可以自己定义。

    在activity.java中

     1 public void onCreate(Bundle savedInstanceState) {
     2     super.onCreate(savedInstanceState);
     3     requestWindowFeature(Window.FEATURE_NO_TITLE);
     4 
     5     setContentView(R.layout.main);
     6 
     7     /* push slider 初始化 start*/
     8     mPushView = (PushSlider)findViewById(R.id.push_view);
     9     int screenWidth = getWindowManager().getDefaultDisplay().getWidth();
    10     mPushView.setPageWidth(PushSlider.SLIDER_PAGE_LEFT, screenWidth/2);
    11     mPushView.setPageWidth(PushSlider.SLIDER_PAGE_RIGHT, screenWidth);
    12     mPushView.setOnPageChangedListener(this);
    13     /* push slider 初始化 end*/
    14 
    15     /* 其他初始化 */
    16 
    17 } 

    5行,把main.xml设为contect view;

    7~13行,获得PushSlider对象句柄,设置左右两个view的宽度,设置焦点切换监听。


    oh~~~ 全篇完~~

    新年攒人品,稍后发源码,还不知道怎么上传源码呢

  • 相关阅读:
    Shell 字符串处理
    Shell 变量替换及测试
    ARTS(一)
    instanceof & isAssignableFrom的异同
    mysql 分组排序取最值
    guava-retrying 源码解析(阻塞策略详解)
    guava-retrying 源码解析(时间限制策略)
    guava-retrying 源码解析(停止策略详解)
    guava-retrying 源码解析(等待策略详解)
    guava-retrying 源码解析(导入项目)
  • 原文地址:https://www.cnblogs.com/inkheart0124/p/3534165.html
Copyright © 2020-2023  润新知