Android事件分发机制,一篇文章就够了!

说起Android事件分发,网上大大小小的文章不胜枚举,最近项目中遇到了些事件冲突的问题,发现自己对Android事件分发机制掌握的还不够好,于是最近整体学习了一波,虽然不是多么高端的技术,但作为Android知识图谱里的重要一环,必须得牢固掌握,下面这张图是前几天学习过程中记录整理的:

事件分发.png

Android事件分发简单的来说就是当手指触摸屏幕之后,产生的一系列ACTON_DOWN,ACTON_MOVE(多个),ACTON_UP等事件的去向,一般我们所分析的是Action_down事件的分发过程,因为move和up事件与其基本相同,对于App开发来说,最直观的页面就是Activity,事件分发是从Activity开始一直延伸到最深处的view,形如Activity-ViewGroup-View的形式。

事件分发主要设计三个重要的方法:dispatchTouchEvent,onInterceptTouchEvent,onTouchEvent,就像它们的名字一样,dispatchTouchEvent是最先调用的方法,它负责事件的分发工作,onInterceptTouchEvent方法只存在于ViewGroup中(View中不存在onInterceptTouchEvent方法),表示是否拦截Event事件,onTouchEvent方法就是具体处理事件地方了。

1.Activity事件分发

下面我们就从Activity说起,当我们点击屏幕的时候,就会产生Event事件,此时会首先调用Activity的dispatchTouchEvent方法,源码如下:

public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
    }

从上面的代码我们可以看出,如果是ACTION_DOWN事件,会调用onUserInteraction()方法,onUserInteraction方法的作用是当触屏点击按home,back,menu键等都会触发此方法。下拉statubar、旋转屏幕、锁屏不会触发此方法,所以可以用在屏保应用上。

紧接着调用了getWindow().superDispatchTouchEvent(ev),即调用了Window的superDispatchTouchEvent,Window是抽象类,其子类为PhoneWindow,找到此方法源码如下:

@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
      return mDecor.superDispatchTouchEvent(event);
}

调用的是mDecor的superDispatchTouchEvent方法,mDecor是DecorView类型,继续查看其源码:

public boolean superDispatchTouchEvent(MotionEvent event) {
        return super.dispatchTouchEvent(event);
}

DecorView调用的是其父类,它的父类是Framelayout,Framelayout并没有重写dispatchTouchEvent方法,所以最终调用的自然是其父类ViewGroup的dispatchTouchEvent.
在分析ViewGroup事件分发之前,先给出Activty的事件分发结论:

Activty事件分发很简单,如果重写了dispatchTouchEvent方法,无论是返回值是false还是true,此时事件分发结束,即不再向下传递,只有返回super.dispatchTouchEvent()时,才会正常往下传递;

2.ViewGroup事件分发

Activity如果没有消费事件,就会传递给ViewGroup来处理,此时首先会调用ViewGroup的dispatchTouchEvent()方法,精简源码如下:

public boolean dispatchTouchEvent(MotionEvent ev) {

    if (onFilterTouchEventForSecurity(ev)) {
        final int action = ev.getAction();
        final int actionMasked = action & MotionEvent.ACTION_MASK;
        ---> 1
        if (actionMasked == MotionEvent.ACTION_DOWN) {
            cancelAndClearTouchTargets(ev);
            //会清空mFirstTouchTarget,重置FLAG_DISALLOW_INTERCEPT
            resetTouchState();
        }

        //intercepted表示是否拦截事件
        final boolean intercepted;
        //如果是ACTION_DOWN事件或者mFirstTouchTarget!=null,进入条件,当一条事件序列来临时,mFirstTouchTarget默认为null
        if (actionMasked == MotionEvent.ACTION_DOWN
                || mFirstTouchTarget != null) {
            final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
            //判断disallowIntercept的值,此值是子view通过调用getParent().requestDisallowInterceptTouchEvent(flag)
            //来控制的,即主动要求父控件是否拦截,true表示不要拦截,false表示拦截
            //注意:requestDisallowInterceptTouchEvent控制的是下次event事件,当前的事件(一般指Action_down)是无法控制的
            if (!disallowIntercept) {
                //不拦截则调用onInterceptTouchEvent方法,根据其返回值决定是否拦截
                intercepted = onInterceptTouchEvent(ev);
                ev.setAction(action);
            } else {
                intercepted = false;
            }
        } else {
            intercepted = true;
        }

        if (!canceled && !intercepted) {

            if (actionMasked == MotionEvent.ACTION_DOWN
                    || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                    || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                final int childrenCount = mChildrenCount;
                if (newTouchTarget == null && childrenCount != 0) {
                    ---> 2
                    //循环遍历子view
                    for (int i = childrenCount - 1; i >= 0; i--) {
                        final int childIndex = getAndVerifyPreorderedIndex(
                                childrenCount, i, customOrder);
                        final View child = getAndVerifyPreorderedView(
                                preorderedList, children, childIndex);

                        if (childWithAccessibilityFocus != null) {
                            if (childWithAccessibilityFocus != child) {
                                continue;
                            }
                            childWithAccessibilityFocus = null;
                            i = childrenCount - 1;
                        }

                        if (!canViewReceivePointerEvents(child)
                                || !isTransformedTouchPointInView(x, y, child, null)) {
                            ev.setTargetAccessibilityFocus(false);
                            continue;
                        }

                        //找到child view
                        newTouchTarget = getTouchTarget(child);
                        if (newTouchTarget != null) {
                            newTouchTarget.pointerIdBits |= idBitsToAssign;
                            break;
                        }
                        resetCancelNextUpFlag(child);
                        //根据dispatchTransformedTouchEvent返回值(返回值由该子View的dispatchTouchEvent决定)判断子view是否处理该事件
                        --->3
                        if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                            mLastTouchDownX = ev.getX();
                            mLastTouchDownY = ev.getY();
                            newTouchTarget = addTouchTarget(child, idBitsToAssign);
                            alreadyDispatchedToNewTouchTarget = true;
                            break;
                        }
                    }
                }

                if (newTouchTarget == null && mFirstTouchTarget != null) {
                    newTouchTarget = mFirstTouchTarget;
                    while (newTouchTarget.next != null) {
                        newTouchTarget = newTouchTarget.next;
                    }
                }
            }
        }
        //没有找到子view
        --->4
        if (mFirstTouchTarget == null) {
            handled = dispatchTransformedTouchEvent(ev, canceled, null,
                    TouchTarget.ALL_POINTER_IDS);
        } else {
            TouchTarget predecessor = null;
            TouchTarget target = mFirstTouchTarget;
            while (target != null) {
                final TouchTarget next = target.next;
                if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                    handled = true;
                } else {
                    final boolean cancelChild = resetCancelNextUpFlag(target.child)
                            || intercepted;
                    if (dispatchTransformedTouchEvent(ev, cancelChild,
                            target.child, target.pointerIdBits)) {
                        handled = true;
                    }
                    if (cancelChild) {
                        if (predecessor == null) {
                            mFirstTouchTarget = next;
                        } else {
                            predecessor.next = next;
                        }
                        target.recycle();
                        target = next;
                        continue;
                    }
                }
                predecessor = target;
                target = next;
            }
        }
        //事件cancel,up等重置一些变量
        if (canceled
                || actionMasked == MotionEvent.ACTION_UP
                || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
            resetTouchState();
        } else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
            final int actionIndex = ev.getActionIndex();
            final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
            removePointersFromTouchTargets(idBitsToRemove);
        }
    }

    return handled;
}

一开始判断了onFilterTouchEventForSecurity()的返回值,关于onFilterTouchEventForSecurity的作用,点击此处查看,-->1处可以看到,当ACTION_DOWN事件来临时,会进行把一些标志位清空或者恢复初始状态,mFirstTouchTarget就是在resetTouchState()方法里进行清空的,那么mFirstTouchTarget是在哪里赋值的呢,往下寻找可以发现,当找到接收Event事件的子View时 ,mFirstTouchTarget被赋值。

紧接着一个if判断,条件是actionMasked == MotionEvent.ACTION_DOWN
或者mFirstTouchTarget != null,ACTION_DOWN事件一开始肯定为true,因为最开始产生的Event事件就是它,进入判断之后,有一个disallowIntercept变量,它的值由(mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0来决定,那么它又是干嘛的呢,通过搜索我们发现它是在requestDisallowInterceptTouchEvent方法中进行赋值的;

requestDisallowInterceptTouchEvent方法的作用就是子View可以控制父View是否拦截事件,子view通过调用getParent().requestDisallowInterceptTouchEvent(true/false)方法来控制父view是否拦截事件,true是不拦截,false是拦截

默认情况下是false,所以此时会调用onInterceptTouchEvent方法来进行拦截操作,true表示拦截事件,交由ViewGroup自身的onTouchEvent来处理该事件,注意,一旦onInterceptTouchEvent返回true之后,在此事件序列中,之后的所有事件不再调用onInterceptTouchEvent,而是直接交由自身onTouchEvent处理,为什么呢?看这一小段代码:

 if (actionMasked == MotionEvent.ACTION_DOWN
                || mFirstTouchTarget != null) {
            final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
            if (!disallowIntercept) {
                intercepted = onInterceptTouchEvent(ev);
                ev.setAction(action);
            } else {
                intercepted = false;
            }
        } else {
            intercepted = true;
        }
}

因为要想执行onInterceptTouchEvent方法,actionMasked == MotionEvent.ACTION_DOWN
或者mFirstTouchTarget != null必须满足其中之一,ACTION_DOWN肯定是false,因为后续事件不会再出现ACTION_DOWN事件,mFirstTouchTarget上面说了,是找到子view后才会赋值,此时还没开始找子view,父view就自己来处理事件了,所以mFirstTouchTarget此时为null,两个条件都不满足,不再继续调用onInterceptTouchEvent拦截后续事件,而是直接intercepted为true,交由自身onTouchEvent处理;

-->2处的代码表示开始循环遍历子View,寻找当前触摸的子view,找到之后,--->3处的代码通过判断dispatchTransformedTouchEvent方法的返回值来决定事件的流向,dispatchTransformedTouchEvent方法精简源码如下:

private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
          View child, int desiredPointerIdBits) {
            final boolean handled;
            if (child == null) {
                  handled = super.dispatchTouchEvent(event);
            } else {
                  handled = child.dispatchTouchEvent(event);
            }
            return handled;
}

可以看到,无论找不找的到Child View,都会调用View的dispatchTouchEvent(event)方法,此时事件分发由ViewGroup传递到View,--->4处的代码表示,如果没有子View,调用dispatchTransformedTouchEvent方法时child会传递null,接着会调用 super.dispatchTouchEvent(event),走默认的事件分发逻辑,最终还是会传递到自身来处理,因为super.dispatchTouchEvent(event)默认是不处理事件的,致此,ViewGroup的事件分发分析完毕,下面就是View的事件分发了。

3.View事件分发

对于View来说,是没有拦截一说的,即没有onInterceptTouchEvent方法,当View接收到Event事件的时候,同样会先触发dispatchTouchEvent方法,dispatchTouchEvent方法精简版源码如下:

public boolean dispatchTouchEvent(MotionEvent event) {
        boolean result = false;
     
        if (onFilterTouchEventForSecurity(event)) {
            if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
                result = true;
            }
            ListenerInfo li = mListenerInfo;
            //如果控件设置了mOnTouchListener,并且控件是ENABLED状态的,并且onTouch的返回值为true,则会拦截此事件,不再继续分发。
            if (li != null && li.mOnTouchListener != null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                result = true;
            }
            //如果控件没有设置mOnTouchListener,返回值就由自身的onTouchEvent决定
            if (!result && onTouchEvent(event)) {
                result = true;
            }
        }

        if (!result && mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
        }

        if (actionMasked == MotionEvent.ACTION_UP ||
                actionMasked == MotionEvent.ACTION_CANCEL ||
                (actionMasked == MotionEvent.ACTION_DOWN && !result)) {
            stopNestedScroll();
        }

        return result;
    }

通过上面的代码可以看出,onTouch方法是先于onTouchEvent执行的,如果View设置了mOnTouchListener,并且控件是ENABLED状态的,并且onTouch的返回值为true,则会拦截此事件,不再继续分发。如果View没有设置mOnTouchListener,返回值就由自身的onTouchEvent决定,onTouchEvent返回true表示自己消费事件,否则不消费,继续传递。

我们再来看下onTouchEvent的关键源码:

public boolean onTouchEvent(MotionEvent event) {
        final float x = event.getX();
        final float y = event.getY();
        final int viewFlags = mViewFlags;
        final int action = event.getAction();
        //控件是否可以点击
        final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
                || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
        //如果控件的状态是DISABLED的,那么直接返回可点击的状态clickable
        if ((viewFlags & ENABLED_MASK) == DISABLED) {
            if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
                setPressed(false);
            }
            mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
            return clickable;
        }
        //控件可点击则进入判断
        if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
            switch (action) {
                case MotionEvent.ACTION_UP:
                    if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
                        boolean focusTaken = false;
                        if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                            removeLongPressCallback();
                            //触发onClick事件
                            if (!focusTaken) {
                                if (mPerformClick == null) {
                                    mPerformClick = new PerformClick();
                                }
                                if (!post(mPerformClick)) {
                                    performClick();
                                }
                            }
                        }
                        removeTapCallback();
                    }
                    mIgnoreNextUpEvent = false;
                    break;

                case MotionEvent.ACTION_DOWN:
                    ......
                    //检测长按事件
                    setPressed(true, x, y);
                    checkForLongClick(0, x, y);
                    ......
                    break;
                case MotionEvent.ACTION_CANCEL:
                    //进行一些重置操作
                    break;

                case MotionEvent.ACTION_MOVE:
                    if (!pointInView(x, y, mTouchSlop)) {
                        //移除长按等回调
                        removeTapCallback();
                        removeLongPressCallback();
                    }
                    break;
            }

            return true;
        }

        return false;
    }

首先检测View是否是clickable,以及是否禁用等逻辑,如果控件可用可点击的状态下,进入switch判断,ACTION_DOWN主要检测长按事件的触发,主要逻辑在ACTION_UP中,ACTION_UP中执行了performClick()方法,如果设置了mOnClickListener监听,则会触发onClick事件;如果控件不可点击,则直接返回false,不拦截,事件继续传递,交由;

onLongClick是在什么时候调用的呢?

其实就是在ACTIOIN_DOWN开启长按检测,执行顺序是checkForLongClick() ->performLongClick(mX, mY)->performLongClick() ->performLongClickInternal,一旦达到长按的阈值(private static final int DEFAULT_LONG_PRESS_TIMEOUT = 500ms),就会调用如下方法:

private boolean performLongClickInternal(float x, float y) {
        boolean handled = false;
        final ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnLongClickListener != null) {
            //关键代码,此处触发了onLongClick回调
            handled = li.mOnLongClickListener.onLongClick(View.this);
        }
        return handled;
    }

所以View的事件分发顺序是:dispatchTouchEvent->onTouch->onTouchEvent->on(Long)Click。

4.为什么onInterceptTouchEvent返回true可以拦截事件?

首先当onInterceptTouchEvent返回true的时候,

if (actionMasked == MotionEvent.ACTION_DOWN
        || mFirstTouchTarget != null) {
    final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
    if (!disallowIntercept) {
            //看这里
            intercepted = onInterceptTouchEvent(ev);
            ev.setAction(action); 
    } else {
            intercepted = false;
    }
}  else {
        intercepted = true;
}

即intercepted的值为ture,那么此时:if (!canceled && !intercepted) {}这个判断是进不去的,也就无法传递到子view了,因为这个判断里的逻辑就是循环遍历子view,进行事件分发。

继续往下看,因为并未进入if (!canceled && !intercepted) {}判断,所以此时mFirstTouchTarget为null,会执行dispatchTransformedTouchEvent方法,由于事件并未分发到子View,所以此时参数child为null:

private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
        View child, int desiredPointerIdBits) {
    final boolean handled;
    if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
        if (child == null) {
              handled = super.dispatchTouchEvent(event);
        } else {
              handled = child.dispatchTouchEvent(event);
        }
        return handled;
    }
}

通过上面代码可以知道,当child为null的时候,会执行super.dispatchTouchEvent(event),也就是会调用自身父类的dispatchTouchEvent方法,也就是View的dispatchTouchEvent方法,而在View的dispatchTouchEvent方法中:

if (onFilterTouchEventForSecurity(event)) {
    if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
        result = true;
    }
    ListenerInfo li = mListenerInfo;
    if (li != null && li.mOnTouchListener != null
            && (mViewFlags & ENABLED_MASK) == ENABLED
            && li.mOnTouchListener.onTouch(this, event)) {
        result = true;
    }

    if (!result && onTouchEvent(event)) {
          result = true;
    }
}

可以看到,如果控件没有设置onTouch相关监听的话,就会去调用 onTouchEvent(event)方法,这就是onInterceptTouchEvent返回true分发到onTouchEvent的逻辑,如果设置了mOnTouchListener,并且返回了true,那么onTouchEvent方法是不会调用的。

5.滑动冲突的一般解决方式

1.通过重写父类ViewGroup的onInterceptTouchEvent方法,根据具体业务需求来决定是否要对事件进行拦截,注意:ACTION_DOWN事件不要返回true,否则子view是无法接收到事件的。

2.子View通过调用getParent().requestDisallowInterceptTouchEvent(true/false)来干预父ViewGroup的onInterceptTouchEvent的事件分发过程。

6.一些重要结论

1.当onInterceptTouchEvent返回ture时,若onTouchEvent返回true,后续事件将不再经过该ViewGroup的onInterceptTouchEvent方法,直接交由该ViewGroup的onTouchEvent方法处理;若onTouchEvent方法返回false,后续事件都将交由父ViewGroup处理,不再经过该ViewGroup的onInterceptTouchEvent方法和onTouchEvent方法。

2.当onInterceptTouchEvent返回false时,事件继续向子View分发;

3.对于子View,当onTouchEvent返回true,父ViewGroup派发过来的touch事件已被该View消费,后续事件不会再向上传递给父ViewGroup,后续的touch事件都将继续传递给子View。

4.对于子View,onTouchEvent返回false,表明该View并不消费父ViewGroup传递来的down事件,而是向上传递给父ViewGroup来处理;后续的move、up等事件将不再传递给该View,直接由父ViewGroup处理掉。

5.onTouch先于onTouchEvent调用,onClick事件是在onTouchEvent中ACTION_UP中触发的。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容