Android动态切换多个View的可编辑/不可编辑模式

前言

在android开发中,经常遇到这样的需求:使一组view动态的在“可编辑”/“不可编辑”两种状态中相互切换,多用于Fragment或者一组View的复用。如何快速实现这样的需求呢?本文介绍一种非常简单的实现方法。

背景

有这样一个布局文件:

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
        代码省略...
     >

    <ScrollView
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <RelativeLayout
            android:id="@+id/container_view "
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical">
            <!-- 可点击/可编辑的控件 -->
            <View ...>
            <View ...>

            <!--  ①可在此处添加coverView   -->
        </RelativeLayout>
    </ScrollView>

</FrameLayout>

需求是动态的切换container_view中所包含的View的状态,使其同时均可以编辑,或者均不可以编辑。

实现方法

常规思路

首先想到的是,在ScrollView中遍历所有可以编辑/点击的控件,如果处于不可编辑模式,就将其设置为disable。但是这种做法有两处弊端:
1.麻烦,需要遍历逐个设置控件属性;
2.设置disable后,与之前的非disable状态的显示效果有区别;

简洁方法

一种比较简便的实现方法是:
在上述布局的最后添加一个可以屏蔽点击事件的view,并使其宽高等于父控件RelativeLayout(称之为containerView)的宽高。

一开始我的思路是直接加一个view(称之为coverView),并使其属性

    android:clickable="false"

并在布局显示出来后,在view.post(Runnable runnable)的runnable中获取container的宽高,并将后来发现这种做法无法实现“覆盖屏蔽下层点击事件”。阅读view源码发现对clickable属性的理解有误,View的onTouchEvent函数如下:

public boolean onTouchEvent(MotionEvent event) {
    final float x = event.getX();
    final float y = event.getY();
    final int viewFlags = mViewFlags;

    if ((viewFlags & ENABLED_MASK) == DISABLED) {
        if (event.getAction() == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
            setPressed(false);
        }
        // A disabled view that is clickable still consumes the touch
        // events, it just doesn't respond to them.
        return (((viewFlags & CLICKABLE) == CLICKABLE ||
                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE));
    }

    if (mTouchDelegate != null) {
        if (mTouchDelegate.onTouchEvent(event)) {
            return true;
        }
    }

    if (((viewFlags & CLICKABLE) == CLICKABLE ||
            (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_UP:
                boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
                if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
                    // take focus if we don't have it already and we should in
                    // touch mode.
                    boolean focusTaken = false;
                    if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
                        focusTaken = requestFocus();
                    }

                    if (prepressed) {
                        // The button is being released before we actually
                        // showed it as pressed.  Make it show the pressed
                        // state now (before scheduling the click) to ensure
                        // the user sees it.
                        setPressed(true, x, y);
                   }

                    if (!mHasPerformedLongPress) {
                        // This is a tap, so remove the longpress check
                        removeLongPressCallback();

                        // Only perform take click actions if we were in the pressed state
                        if (!focusTaken) {
                            // Use a Runnable and post this rather than calling
                            // performClick directly. This lets other visual state
                            // of the view update before click actions start.
                            if (mPerformClick == null) {
                                mPerformClick = new PerformClick();
                            }
                            if (!post(mPerformClick)) {
                                performClick();
                            }
                        }
                    }

                    if (mUnsetPressedState == null) {
                        mUnsetPressedState = new UnsetPressedState();
                    }

                    if (prepressed) {
                        postDelayed(mUnsetPressedState,
                                ViewConfiguration.getPressedStateDuration());
                    } else if (!post(mUnsetPressedState)) {
                        // If the post failed, unpress right now
                        mUnsetPressedState.run();
                    }

                    removeTapCallback();
                }
                break;

            case MotionEvent.ACTION_DOWN:
                mHasPerformedLongPress = false;

                if (performButtonActionOnTouchDown(event)) {
                    break;
                }

                // Walk up the hierarchy to determine if we're inside a scrolling container.
                boolean isInScrollingContainer = isInScrollingContainer();

                // For views inside a scrolling container, delay the pressed feedback for
                // a short period in case this is a scroll.
                if (isInScrollingContainer) {
                    mPrivateFlags |= PFLAG_PREPRESSED;
                    if (mPendingCheckForTap == null) {
                        mPendingCheckForTap = new CheckForTap();
                    }
                    mPendingCheckForTap.x = event.getX();
                    mPendingCheckForTap.y = event.getY();
                    postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
                } else {
                    // Not inside a scrolling container, so show the feedback right away
                    setPressed(true, x, y);
                    checkForLongClick(0);
                }
                break;

            case MotionEvent.ACTION_CANCEL:
                setPressed(false);
                removeTapCallback();
                removeLongPressCallback();
                break;

            case MotionEvent.ACTION_MOVE:
                drawableHotspotChanged(x, y);

                // Be lenient about moving outside of buttons
                if (!pointInView(x, y, mTouchSlop)) {
                    // Outside button
                    removeTapCallback();
                    if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
                        // Remove any future long press/tap checks
                        removeLongPressCallback();

                        setPressed(false);
                    }
                }
                break;
        }

        return true;
    }

    return false;
}

可以看到有这样一行代码:

    if (((viewFlags & CLICKABLE) == CLICKABLE ||
            (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)){ 

            //省略代码...

            return true
    }

即:当此view是clickable时,touch事件将不会继续传递,这才是我想要的结果。

如下所示布局:

<View
    android:id="@+id/cover_view"
    android:layout_width="match_parent"
    android:layout_height="0dp"
    android:background="#00000000"
    android:clickable="true"/>

将此view添加到containerView中(在本例中添加到上述布局文件的①处),并在view显示时使用post(Runnable r)函数根据containerView的宽高重新设置cover_view的宽高,java代码如下:

private void setCoverIfNotEditable(View containerView,View coverView, boolean editable){
    if(editable){
        coverView.setVisibility(View.GONE);
    }else{
        containerView.post(new Runnable() {
            @Override
            public void run() {
                ViewGroup.LayoutParams params = coverView.getLayoutParams();
                params.width = containerView.getMeasuredWidth();
                params.height = containerView.getMeasuredHeight();
                coverView.setLayoutParams(params);
                coverView.setVisibility(View.VISIBLE);
            }
        });
    }
}

功能完成。

另一种类似解决方案:自定义view
既然最核心的是屏蔽触摸事件向下传递,那么可以自定义view,将其onTouchEvent事件返回true。代码如下:

    /**
 * created by Carbs.Wang
 */
public class CoverView extends View {

    public CoverView(Context context) {
        super(context);
    }
    public CoverView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
    public CoverView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        return true;
    }
}

然后在布局中使用此view作为coverView,替换上述的clickable=”true”的coverView即可

    <cn.carbs.android.CoverView
                android:id="@+id/cover"
                android:layout_width="match_parent"
                android:layout_height="0dp"
                android:background="#00000000"/>

总结

总结一下实现的方法以及注意事项:
1. containerView必须具有这样的属性:其某一个子view的变大变小,可以不影响其他子view的位置。如RelativeLayout或者FrameLayout。LinearLayout不行。
2. coverView初始设置为不可见或者0dp,并在页面view解析出来后,获取containerView的宽高,推荐使用view.post()这种方式获取view宽高,并将宽高设置到coverView中。
3. coverView在显示时,应当屏蔽触摸事件继续传递,如果不屏蔽,则Z-order底部的view仍然可以获取消息,无法实现需求。
4. 屏蔽事件继续传递有两种方式:一是为View添加clickable=”true”的属性;二是自定义View,使其onTouchEvent返回true,即自己消费了触摸事件,不会继续传递此事件。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值