为了账号安全,请及时绑定邮箱和手机立即绑定

自定义View同时显示3个Fragment并自由切换

标签:
Android

工作中需要实现如下的一个效果,有三个界面,两边的界面都漏出一部分来,点击两边或者在中间滑动就可以让旁边的界面同中间的进行交换.要怎么来实现这个效果呢?

1. 思路

考虑到这三个界面互相独立而且相对有各自的业务,混在一起的话很乱,而且以后如果要替换某个界面会很麻烦(千万不要低估产品同学们改来改去的决心). 所以我们准备使用3个Fragment来分别实现3个界面的内容,在各个Fragment内部完成界面的渲染和数据的请求等. 那我们就需要三个Layout排列成图中的样子,然后将Fragment添加进去就可以了.

2. 实现

有了思路就开始干吧.一开始的想法是在一个RelativeLayout里面放上3个Layout, 并分别进行定位, 结果发现两边的Layout并不会伸到屏幕的外面去,而是都积压到一起, 完全变形了. 看来只能自定义View并手动将里面的Layout给添加进去了.

2.1 布局定位

我们自定义一个ViewSwitcherView继承自RelativeLayout(其它的也可以), 起名为SwitcherView吧. 在使用的时候让其包含3个子view:

[代码]xml代码:

?

01

02

03

04

05

06

07

08

09

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

<com.mushuichuan.threefragmetsswitcher.SwitcherView

    android:id="@+id/switcherview"

    xmlns:android="http://schemas.android.com/apk/res/android"

    xmlns:app="http://schemas.android.com/apk/res-auto"

    android:layout_width="match_parent"

    android:layout_height="match_parent"

    >

    <FrameLayout

        android:id="@+id/child_middle"

        android:layout_width="wrap_content"

        android:layout_height="match_parent"

        android:background="@android:color/holo_blue_dark"/>

 

    <FrameLayout

        android:id="@+id/child_left"

        android:layout_width="wrap_content"

        android:layout_height="match_parent"

        android:background="@android:color/holo_green_dark"/>

 

    <FrameLayout

        android:id="@+id/child_right"

        android:layout_width="wrap_content"

        android:layout_height="match_parent"

        android:background="@android:color/holo_red_dark"/>

</com.mushuichuan.threefragmetsswitcher.SwitcherView>

 

这样我们在SwitcherView内部就有了3个子View, 将它们find出来:

[代码]java代码:

?

1

2

3

4

5

void initChildView() {

    mChildMiddle =   (FrameLayout) findViewById(R.id.child_middle);

    mChildLeft =   (FrameLayout) findViewById(R.id.child_left);

    mChildRight =   (FrameLayout) findViewById(R.id.child_right);

}

 

在ViewGroup中有一个onLayout方法, 当需要给子View进行定位和指定大小的时候就会调用, 那我们就可以在这个方法里面对这三个子View进行定位了. 调用的时候会将SwitcherView四个角的值传进来, 我们可以用这四个值计算子View的位置. 计算出每一个子View的位置后, 调用其layout方法就可以对子View进行定位了. 最后我们还需要将三个子View的LayoutParams给保存下来, 方便我们下一步调换子View的位置时使用.

[代码]java代码:

?

01

02

03

04

05

06

07

08

09

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

@Override

protected void onLayout(boolean changed, int l, int t, int r, int b) {

    super.onLayout(changed,   l, t, r, b);

    if (mChildMiddle == null) {

        initChildView();

    }

    int middleWidth = (int) (r *   middleProportion);

    middleLeft = (r -   middleWidth) / 2;

    middleRight = r -   (r - middleWidth) / 2;

    mChildMiddle.layout(middleLeft,   t + middleMarginTopAndDown, middleRight, b - middleMarginTopAndDown);

 

 

    int leftRight = middleLeft -   middleMarginLeftAndRight;

    int leftLeft = -(middleWidth - leftRight);

    mChildLeft.layout(leftLeft,   t + sideMarginTopAndDown, leftRight, b - sideMarginTopAndDown);

 

    int rightLeft = middleRight +   middleMarginLeftAndRight;

    int rightRight = rightLeft + middleWidth;

    mChildRight.layout(rightLeft,   t + sideMarginTopAndDown, rightRight, b - sideMarginTopAndDown);

 

    mMiddleParam =   (LayoutParams) mChildMiddle.getLayoutParams();

    mLeftParam =   (LayoutParams) mChildLeft.getLayoutParams();

    mRightParam =   (LayoutParams) mChildRight.getLayoutParams();

}

 

2.2 互换位置

由于我们已将三个子View的LayoutParams给保存到了变量中, 所以当我们需要更换两个子View的位置时, 我们只需要将他们的LayoutParams的换一下就可以达到目的. 在这里我们还添加了动画, 让交互更好一些.

[代码]java代码:

?

01

02

03

04

05

06

07

08

09

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

public void switchLeftAndMiddle()   {

    Animation   leftInAnimation = AnimationUtils.loadAnimation(getContext(),   R.anim.slide_left_in);

    mChildLeft.startAnimation(leftInAnimation);

    Animation   leftOutAnimation = AnimationUtils.loadAnimation(getContext(),   R.anim.slide_left_out);

    mChildMiddle.startAnimation(leftOutAnimation);

    leftOutAnimation.setAnimationListener(new Animation.AnimationListener() {

        @Override

        public void onAnimationStart(Animation animation) {

 

        }

 

        @Override

        public void onAnimationEnd(Animation animation) {

            mChildMiddle.setLayoutParams(mLeftParam);

            mChildLeft.setLayoutParams(mMiddleParam);

            FrameLayout   temp = mChildMiddle;

            mChildMiddle   = mChildLeft;

            mChildLeft   = temp;

        }

 

        @Override

        public void onAnimationRepeat(Animation animation) {

 

        }

    });

 

}

 

public void switchRightAndMiddle()   {

    Animation   leftInAnimation = AnimationUtils.loadAnimation(getContext(),   R.anim.slide_right_in);

    mChildRight.startAnimation(leftInAnimation);

    Animation   rightOutAnimation = AnimationUtils.loadAnimation(getContext(),   R.anim.slide_right_out);

    mChildMiddle.startAnimation(rightOutAnimation);

    rightOutAnimation.setAnimationListener(new Animation.AnimationListener() {

        @Override

        public void onAnimationStart(Animation animation) {

 

        }

 

        @Override

        public void onAnimationEnd(Animation animation) {

            mChildMiddle.setLayoutParams(mRightParam);

            mChildRight.setLayoutParams(mMiddleParam);

            FrameLayout   temp = mChildMiddle;

            mChildMiddle   = mChildRight;

            mChildRight   = temp;

        }

 

        @Override

        public void onAnimationRepeat(Animation animation) {

 

        }

    });

 

}

 

2.3 添加手势

最后就是添加手势实现点击两边或者中间滑动实现子View之间的互换. 我们重写了onTouchEvent方法, touch down 的时候记录下其x坐标, touch up的时候再获取其x坐标, 两者取其差的绝对值, 超过某个范围就认为其滑动了, 否则认为其为一个点击事件.

[代码]java代码:

?

01

02

03

04

05

06

07

08

09

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

@Override

public boolean onTouchEvent(MotionEvent   event) {

    Log.d(TAG, "onTouchEvent:" + event.toString());

    switch (event.getAction()) {

        case MotionEvent.ACTION_DOWN: {

            startX   = event.getX();

            Log.d(TAG,   "startx:" + startX);

            return true;

        }

        case MotionEvent.ACTION_UP: {

            endX   = event.getX();

            Log.d(TAG,   "endX:" + endX);

            if (abs(endX - startX) < CLICK_THRESHOLD)   {

                if (startX < middleLeft) {

                    switchLeftAndMiddle();

                    return true;

                }   else if (startX > middleRight) {

                    switchRightAndMiddle();

                    return true;

                }

            }   else {

                if (endX > startX) {

                    switchRightAndMiddle();

                    return true;

                }   else if (endX < startX) {

                    switchLeftAndMiddle();

                    return true;

                }

            }

            break;

        }

    }

    return false;

}

 

似乎实现要求的功能了, 但是使用时发现如果两边的Fragment里面有实现对点击事件的监听, 我们这里就监听不到点击事件了, 所以需要对两边的点击事件进行拦截. 我们重写了onIntercepTouchEvent方法来实现这个拦截. 当有touch down的事件时, 我们判断一下其点击的区域, 如果处于两边的边缘区域, 则返回true, 代表这次的点击事件被我们的SwitcherView给拦截了, 不会再向下分发.

[代码]java代码:

?

1

2

3

4

5

6

7

8

@Override

public boolean onInterceptTouchEvent(MotionEvent   ev) {

    if (ev.getAction() ==   MotionEvent.ACTION_DOWN) {

        if (ev.getX() < middleLeft || ev.getX()   > middleRight)

            return true;

    }

    return false;

}

 

3. 完善

Ok, 功能都实现了, 但是还有一些细节没做好, 如中间这个Layout的宽度占屏幕宽度的比例, 两边的Layout同中间的间隔大小, 及上线的间隔等. 如果改这些每次都要改源码那可麻烦死了, 而且这个View可能被用在多个不同的地方. 所以我们来对SwitcherView添加几个属性吧, 在使用的时候根据实际情况进行配置.

首先在values目录下创建一个文件attrs.xml, 将我们要添加的属性添加到这个文件中, 在这里我们定义了4个属性, 并分别指定属性的类型:

[代码]xml代码:

?

1

2

3

4

5

6

7

8

9

<?xml version="1.0" encoding="utf-8"?>

<resources>

<declare-styleable name="SwticherView">

    <attr name="middleProportion" format="float"></attr>

    <attr name="sideMarginTopAndDown" format="dimension"></attr>

    <attr name="middleMarginLeftAndRight" format="dimension"></attr>

    <attr name="middleMarginTopAndDown" format="dimension"></attr>

</declare-styleable>

</resources>

 

然后在SwticherView中读取这些参数, 读取到的参数就可以用在代码里来进行各种配置了:

[代码]java代码:

?

1

2

3

4

5

6

7

8

9

public SwitcherView(Context context, AttributeSet attrs, int defStyleAttr) {

    super(context,   attrs, defStyleAttr);

    TypedArray   mTypedArray = context.obtainStyledAttributes(attrs,   R.styleable.SwticherView);

    middleProportion   = mTypedArray.getFloat(R.styleable.SwticherView_middleProportion, 0.75f);

    sideMarginTopAndDown   = mTypedArray.getDimensionPixelSize(R.styleable.SwticherView_sideMarginTopAndDown,   0);

    middleMarginTopAndDown   =   mTypedArray.getDimensionPixelSize(R.styleable.SwticherView_middleMarginTopAndDown,   0);

    middleMarginLeftAndRight   = mTypedArray.getDimensionPixelSize(R.styleable.SwticherView_middleMarginLeftAndRight,   0);

    initChildView();

}

 

使用的时候来指定参数的值:

[代码]xml代码:

?

01

02

03

04

05

06

07

08

09

10

11

<com.mushuichuan.threefragmetsswitcher.SwitcherView

    android:id="@+id/switcherview"

    xmlns:android="http://schemas.android.com/apk/res/android"

    xmlns:app="http://schemas.android.com/apk/res-auto"

    android:layout_width="match_parent"

    android:layout_height="match_parent"

    app:middleMarginLeftAndRight="10dp"

    app:middleMarginTopAndDown="5dp"

    app:middleProportion="0.75"

    app:sideMarginTopAndDown="20dp"

    >

 

4.改进

使用中会发现,如果touch事件被Fragment给消费掉了, 我们的SwitcherView的onTouchEvent方法将接收不到touch事件了. 所以我们不能重写onTouchEvent方法而是重写dispatchTouchEvent方法. 根据Android的事件分发机制, 所有需要传递到Fragment里面的事件都需要经过我们SwitcherView的dispatchTouchEvent的分发, 所以我们可以在这里进行滑动手势的监听. 需要特别注意的是对于ACTION_DOWN的事件一定要返回true, 这样后续的事件才会继续分发到这里.
如果SwitcherView是嵌套到listview里面的, 当滑动的时候经常会触发上下滑动,造成误操作. 当Listview上下滑动的时候,我们会接收到ACTION_CANCEL事件, 所以我们也需要处理一下ACTION_CANCEL事件, 这样即使触发了上下滑动,我们的左右滑动还是可以使用的.

[代码]java代码:

?

01

02

03

04

05

06

07

08

09

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

@Override

public boolean dispatchTouchEvent(MotionEvent   event) {

    Log.i(TAG, "dispatchTouchEvent:" + event.toString());

    boolean handled = super.dispatchTouchEvent(event);

    Log.i(TAG, "handled:" + handled);

 

    switch (event.getAction()) {

        case MotionEvent.ACTION_DOWN: {

            startX   = event.getX();

            Log.i(TAG,   "startx:" + startX);

        }

        break;

        case MotionEvent.ACTION_CANCEL:

        case MotionEvent.ACTION_UP: {

            endX   = event.getX();

            Log.i(TAG,   "endX:" + endX);

            if (abs(endX - startX) < CLICK_THRESHOLD)   {

                //点击事件

                if (startX < middleLeft) {

                    switchLeftAndMiddle();

                }   else if (startX > middleRight) {

                    switchRightAndMiddle();

                }

            }   else {

                //滑动事件

                if (endX > startX) {

                    switchRightAndMiddle();

                }   else if (endX < startX) {

                    switchLeftAndMiddle();

                }

            }

            break;

        }

    }

    return true;

}

 

5.结语

到这里我们就实现了预期的效果了, 来看看实现效果吧:

说明: http://7xp3vb.com1.z0.glb.clouddn.com/_2016_05_26_10_33_52_918.gif

本文中的完整代码请移步Github

6. 再改进

上述的方法还是有局限性,如不能随着手指滑动而滑动,然后又发现了新的方法,这次是在ViewPager的基础上做的。ViewPager已经将滑动实现好了,所以就需要处理一下如何让两边的Fragment也漏出一点来。使用的方法是设置ViewPager及其父ViewGroup的clipChildren属性为false,该属性默认是设为true的,让我们看一下文档中对改属性的解释:

Defines whether a child is limited to draw inside of its bounds or not. This is useful with animations that scale the size of the children to more than 100% for instance. In such a case, this property should be set to false to allow the children to draw outside of their bounds. The default value of this property is true.

就是说是不是要子View局限在它的范围内.如果我们将ViewPager及其父view的这项属性都设为false,那ViewPager里面两边的Fragment也能漏出来了。

[代码]xml代码:

?

01

02

03

04

05

06

07

08

09

10

11

12

13

14

15

16

<RelativeLayout

    android:id="@+id/body"

    xmlns:android="http://schemas.android.com/apk/res/android"

    xmlns:tools="http://schemas.android.com/tools"

    android:layout_width="match_parent"

    android:layout_height="match_parent"

    android:clipChildren="false"

    tools:context="com.mushuichuan.threefragmetsswitcher.MainActivity2">

 

    <android.support.v4.view.ViewPager

        android:id="@+id/view_pager"

        android:layout_width="300dp"

        android:layout_height="match_parent"

        android:layout_centerInParent="true"

        android:clipChildren="false"/>

</RelativeLayout>

 

但是仅仅是漏出来了,如果你点击或者滑动漏出来的地方是不会触发Viewpager的滑动的,这是因为ViewPager还是原来那么大。如果要处理点击两边也要ViewPager滑动的话,就要监听其父View的onTouch事件再进行操作。

最后就是实现两边的Fragment缩小的问题了。ViewPager可以设置PageTransformer,我们可以自定义一个PageTransformer来实现两边缩小的问题。

[代码]java代码:

?

01

02

03

04

05

06

07

08

09

10

11

12

13

14

public class ZoomPageTransformer   implements ViewPager.PageTransformer   {

    private static final float MIN_SCALE = 0.85f;

 

 

    @SuppressLint("NewApi")

    public void transformPage(View view, float position) {

        Log.d("test",   view.getId() + ":" + position);

        if (position <= 1) {

            float scaleFactor = Math.max(MIN_SCALE, 1 - Math.abs(position));

            view.setScaleX(scaleFactor);

            view.setScaleY(scaleFactor);

        }

    }

}

 

原文链接:http://www.apkbus.com/blog-705730-61767.html


点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消