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

Posted carbs_wang

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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,即自己消费了触摸事件,不会继续传递此事件。

以上是关于Android动态切换多个View的可编辑/不可编辑模式的主要内容,如果未能解决你的问题,请参考以下文章

EditView不可编辑状态和可编辑状态动态切换 及 EditView的其它特效

android 动态切换view

Android 逆向arm 汇编 ( 使用 IDA 解析 arm 架构的动态库文件 | 使用 IDA 打开 arm 动态库文件 | 切换 IDA 中汇编代码显示样式 )

Android 逆向arm 汇编 ( 使用 IDA 解析 arm 架构的动态库文件 | 使用 IDA 打开 arm 动态库文件 | 切换 IDA 中汇编代码显示样式 )

怎么实现Android的Spinner控件不可编辑

求助 DevExpress GridControl 控件 想要做一个View 第一行可以编辑 其他行不可编辑