본문 바로가기

IT/Android

ClipToOutline, ClipToPadding, ClipToChildren

728x90
반응형

참고자료 : Create Shadows and Clip Views  |  Android Developers ,

AAPT: error: attribute android:clipToOutline not found ,

RenderNode  |  Android Developers

 

Outline 내부 동작

먼저 outline 이 내부적으로 어떻게 동작하는지 확인해보겠습니다. (sdk level 31 기준)

View.java

    @CallSuper
    protected void onAttachedToWindow() {
        if ((mPrivateFlags & PFLAG_REQUEST_TRANSPARENT_REGIONS) != 0) {
            mParent.requestTransparentRegion(this);
        }

        mPrivateFlags3 &= ~PFLAG3_IS_LAID_OUT;

        jumpDrawablesToCurrentState();

        AccessibilityNodeIdManager.getInstance().registerViewWithId(this, getAccessibilityViewId());
        resetSubtreeAccessibilityStateChanged();

        // rebuild, since Outline not maintained while View is detached
        rebuildOutline();  1️⃣
        if (isFocused()) {
            notifyFocusChangeToImeFocusController(true /* hasFocus */);
        }
    }

1️⃣ View 에서 onAttachedToWindow 에서 rebuildOutline(); 이 불리게 됩니다.

 

View.java

...
ViewOutlineProvider mOutlineProvider = ViewOutlineProvider.BACKGROUND;    3️⃣
...

final static class AttachInfo {
...

        /**
         * Temporary for use in querying outlines from OutlineProviders
         */
        final Outline mTmpOutline = new Outline();
        ...
}
...

    private void rebuildOutline() {
        // Unattached views ignore this signal, and outline is recomputed in onAttachedToWindow()
        if (mAttachInfo == null) return;

        if (mOutlineProvider == null) {     2️⃣
            // no provider, remove outline
            mRenderNode.setOutline(null);
        } else {
            final Outline outline = mAttachInfo.mTmpOutline;  4️⃣      
            outline.setEmpty();   5️⃣            
            outline.setAlpha(1.0f); 

            mOutlineProvider.getOutline(this, outline);   6️⃣
            mRenderNode.setOutline(outline);    9️⃣   
        }
    }

2️⃣ rebuildOutline 에서 mOutlineProvider 가 null 이면 mRenderNode.setOutline(null); 를 호출합니다.

3️⃣ 기본적으로 mOutlineProvider 는 멤버변수로 ViewOutlineProvider.BACKGROUND 값이 할당되어 있습니다.

mOutlineProvider 가 null 이 아니기 때문에 이하 코드가 호출됩니다.

4️⃣ 임시 outline class 를 가져와 5️⃣ setEmpty 와 setAlpha 를 호출합니다.

 

Outline 의 setEmpty 및 setAlpha 메서드는 다음과 같습니다.

더보기
Outline.java


    public void setEmpty() {
        if (mPath != null) {
            // rewind here to avoid thrashing the allocations, but could alternately clear ref
            mPath.rewind();
        }
        mMode = MODE_EMPTY;
        mRect.setEmpty();
        mRadius = RADIUS_UNDEFINED;
    }
    
    public void setAlpha(@FloatRange(from=0.0, to=1.0) float alpha) {
        mAlpha = alpha;
    }

6️⃣ 다시 VIew 로 돌아가서 mOutlineProvider.getOutline 으로 outline 을 가져옵니다.

 

ViewOutlineProvider 클래스

ViewOutlineProvider.java


public abstract class ViewOutlineProvider {

    public static final ViewOutlineProvider BACKGROUND = new ViewOutlineProvider() {
        @Override
        public void getOutline(View view, Outline outline) {
            Drawable background = view.getBackground();   7️⃣
            if (background != null) {
                background.getOutline(outline);
            } else {
                outline.setRect(0, 0, view.getWidth(), view.getHeight());
                outline.setAlpha(0.0f);
            }
        }
    };


    public static final ViewOutlineProvider BOUNDS = new ViewOutlineProvider() {
        @Override
        public void getOutline(View view, Outline outline) {
            outline.setRect(0, 0, view.getWidth(), view.getHeight());
        }
    };


    public static final ViewOutlineProvider PADDED_BOUNDS = new ViewOutlineProvider() {
        @Override
        public void getOutline(View view, Outline outline) {
            outline.setRect(view.getPaddingLeft(),
                    view.getPaddingTop(),
                    view.getWidth() - view.getPaddingRight(),
                    view.getHeight() - view.getPaddingBottom());
        }
    };

7️⃣ 현재 mOutlineProviderViewOutlineProvider BACKGROUND 이기 때문에 view 의 background Drawable 을 가져와서 null 이 아니면 8️⃣ background Drawable의 getOutline 을 호출하고 null 이면 outline 에 view의 크기만큼(width, height) rect 를 set 하고 alpha 값을 0으로 할당합니다.

Drawable 의 getOutline 코드는 다음과 같습니다.

Drwable.java

    public void getOutline(@NonNull Outline outline) {    8️⃣
        outline.setRect(getBounds());
        outline.setAlpha(0);
    }

 

9️⃣ 다시 View로 돌아가서 mRenderNode.setOutline(outline) 을 호출합니다.

 

RenderNode 코드는 다음과 같습니다.

RenderNode.java


    public boolean setOutline(@Nullable Outline outline) {
        if (outline == null) {
            return nSetOutlineNone(mNativeRenderNode);
        }

        switch (outline.mMode) {
            case Outline.MODE_EMPTY:    🔟                
                return nSetOutlineEmpty(mNativeRenderNode);
            case Outline.MODE_ROUND_RECT:   
                return nSetOutlineRoundRect(mNativeRenderNode,
                        outline.mRect.left, outline.mRect.top,
                        outline.mRect.right, outline.mRect.bottom,
                        outline.mRadius, outline.mAlpha);
            case Outline.MODE_PATH:
                return nSetOutlinePath(mNativeRenderNode, outline.mPath.mNativePath,
                        outline.mAlpha);
        }

        throw new IllegalArgumentException("Unrecognized outline?");
    }
    
    
    
    @CriticalNative
    private static native boolean nSetOutlineRoundRect(long renderNode, int left, int top,
            int right, int bottom, float radius, float alpha);

위에서 5️⃣ outline.setEmpty() 에서 🔟 MODE_EMPTY 가 set 되었기 때문에 nSetOutlineEmpty native 함수가 호출됩니다.

 

참고 : RenderNode

RenderNode는 하드웨어 가속 렌더링 계층을 구축하는 데 사용됩니다. 각 RenderNode에는 display list 와 display list 의 렌더링에 영향을 주는 속성 집합이 모두 포함되어 있습니다. RenderNode는 기본적으로 모든 view 에 대해 내부적으로 사용되며 일반적으로 직접 사용되지 않습니다.

RenderNode는 복잡한 장면의 렌더링 콘텐츠를 더 저렴한 비용으로 개별적으로 업데이트할 수 있는 더 작은 조각으로 나누는 데 사용됩니다. 장면의 일부를 업데이트하려면 처음부터 모든 것을 다시 그리는 대신 display list 나 소수의 RenderNode 속성만 업데이트하면 됩니다. RenderNode는 콘텐츠만 변경해야 하는 경우에만 display list 을 다시 기록해야 합니다. 변환 속성을 통해 display list 을 다시 기록하지 않고도 RenderNode를 변환할 수도 있습니다.

예를 들어 텍스트 편집기는 각 단락을 자체 RenderNode에 저장할 수 있습니다. 따라서 사용자가 문자를 삽입하거나 제거할 때 영향을 받는 단락의 표시 display list 만 다시 기록하면 됩니다.

하드웨어 가속

RecordingCanvas 를 사용하여 RenderNode를 그릴 수 있습니다 . 소프트웨어에서는 지원되지 않습니다. display list 을 렌더링하는 데 사용하는 캔버스가 Canvas.isHardwareAccelerated()를 사용하여 하드웨어 가속되었는지 항상 확인하십시오.

 

Outline 을 이용한 구현 방법

  1. ViewOutlineProvider 를 상속받은 CustomOutlineProvider 구현합니다.
  2. 구현한 CustomOutlineProvider 를 target View에 set 해줍니다.
  3. view 의 setClipToOutline 값을 true 로 설정합니다.
  1. CustomOutlineProvider
더보기

getOutline 을 구현한 CustomOutlineProvider 코드는 아래와 같습니다.

CustomOutlineProvider.kt

class CustomOutlineProvider(private val radius: Float) : ViewOutlineProvider() {
    override fun getOutline(view: View?, outline: Outline?) {
        if (view == null) return
        val cornerRadiusDP = radius
        val left = 0
        val top = 0
        val right = view.width
        val bottom = view.height
        val cornerRadius = TypedValue.applyDimension(
            TypedValue.COMPLEX_UNIT_DIP,
            cornerRadiusDP,
            view.resources?.displayMetrics
        )
        outline?.setRoundRect(left, top, right, bottom, cornerRadius)   👈
    }
}

👈 CustomOutlineProvider 의 getOutline 에서 setRoundRect 를 호출합니다.

 

Outline 의 setRoundRect 코드는 다음과 같습니다.

Outline.java


    public void setRoundRect(int left, int top, int right, int bottom, float radius) {
        if (left >= right || top >= bottom) {
            setEmpty();
            return;
        }

        if (mMode == MODE_PATH) {
            // rewind here to avoid thrashing the allocations, but could alternately clear ref
            mPath.rewind();
        }
        mMode = MODE_ROUND_RECT; 👈
        mRect.set(left, top, right, bottom);
        mRadius = radius;
    }

👈 setRoundRect 에서 mMode 를 MODE_ROUND_RECT 로 변경하고, mRect 및 mRadius 를 set 합니다.

내부동작에서 위의 RenderNode 코드를 보면 🔟 에서 MODE_ROUND_RECT 일 때는 nSetOutlineRoundRect 메서드가 호출됩니다. native 메서드라 내부를 볼 수는 없지만 여기서 round 효과를 주는 것임을 추측할 수 있습니다.

 

2. targetView 에 customOutlineProvider set

더보기
fun View.setCustomOutline(radius: Float) {
    val outlineProvider = CustomOutlineProvider(radius)
    this.outlineProvider = outlineProvider    1️⃣    
    this.clipToOutline = true

}

extension 으로 View 에 outlineProvider 를 radius 를 인자로 받아 생성하고 set 하도록 하였습니다.

1️⃣ outlineProvider 를 set 하면 다음이 호출됩니다.

View.java

    public void setOutlineProvider(ViewOutlineProvider provider) {
        mOutlineProvider = provider;
        invalidateOutline();    2️⃣    
    }

2️⃣ outlineProvider 를 mOutlineProvider 에 할당하고 invalidateOutline 함수를 호출합니다.

 

invalidateOutline 은 다음과 같습니다.

View.java

    public void invalidateOutline() {
        rebuildOutline();

        notifySubtreeAccessibilityStateChangedIfNeeded();
        invalidateViewProperty(false, false);
    }

 내부동작에서 설명한 rebuildOutline 이 호출됩니다.

3. view 의 setClipToOutline 값을 true 로 설정합니다

더보기
fun View.setCustomOutline(radius: Float) {
    val outlineProvider = CustomOutlineProvider(radius)
    this.outlineProvider = outlineProvider        
    this.clipToOutline = true   3️⃣
}

3️⃣ 에서 setClipToOutline 함수를 호출합니다.

setClipToOutline 함수는 다음과 같습니다.

View.java

    public void setClipToOutline(boolean clipToOutline) {
        damageInParent();   4️⃣        
        if (getClipToOutline() != clipToOutline) {    5️⃣            
            mRenderNode.setClipToOutline(clipToOutline);
        }
    }

4️⃣ damageInParent 에서 자식 view 가 invalidated 되었음을 알립니다.

 

damageInParent 코드는 다음과 같습니다.

 

    /**
     * Tells the parent view to damage this view's bounds.
     *
     * @hide
     */
    protected void damageInParent() {
        if (mParent != null && mAttachInfo != null) {
            mParent.onDescendantInvalidated(this, this);
        }
    }

5️⃣ getClipToOutline 값과 인자 값이 다르면 mRenderNode 에 새로운 값을 할당해줍니다.

RenderNode.java

    public boolean setClipToOutline(boolean clipToOutline) {
        return nSetClipToOutline(mNativeRenderNode, clipToOutline);
    }

 

장점 : 간단하게 extension 함수를 통해 필요한 View에 적용가능합니다.

단점 : 적용해야 하는 View 가 많을수록 코드 내에서 호출해야 합니다.

 

View 의 Outline 속성을 이용하는 방법

view 에는 outline 과 관련된 2가지 속성이 있습니다.

outlineProvider 와 clipToOutline 입니다.

더보기

View 생성자에서 다음과 같이 맵핑됩니다.

 

View.java

    public View(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        this(context);
        ...
        
                case R.styleable.View_outlineProvider:
                    setOutlineProviderFromAttribute(a.getInt(R.styleable.View_outlineProvider,
                            PROVIDER_BACKGROUND));
                            
                ...
                case R.styleable.View_clipToOutline:
                    setClipToOutline(a.getBoolean(attr, false));
                    break;

 

  • outlineProvider

사용예시는 다음과 같습니다.

android:outlineProvider="background"

background, bounds, paddedBounds, none 4가지 선택이 가능합니다.

선택된 값은 아래 코드와 같이 맵핑됩니다.

View.java

    // correspond to the enum values of View_outlineProvider
    private static final int PROVIDER_BACKGROUND = 0;
    private static final int PROVIDER_NONE = 1;
    private static final int PROVIDER_BOUNDS = 2;
    private static final int PROVIDER_PADDED_BOUNDS = 3;
    private void setOutlineProviderFromAttribute(int providerInt) {
        switch (providerInt) {
            case PROVIDER_BACKGROUND:
                setOutlineProvider(ViewOutlineProvider.BACKGROUND);
                break;
            case PROVIDER_NONE:
                setOutlineProvider(null);
                break;
            case PROVIDER_BOUNDS:
                setOutlineProvider(ViewOutlineProvider.BOUNDS);
                break;
            case PROVIDER_PADDED_BOUNDS:
                setOutlineProvider(ViewOutlineProvider.PADDED_BOUNDS);
                break;
        }
    }

 

  • clipToOutline

사용예시는 다음과 같습니다.

android:clipToOutline="true"

 

한계점 :

  • round outline 을 만들수 없습니다. (지정된 형태의 outline 만 가능합니다)
  • clipToOutline 은 sdk level 31 이상에서 가능합니다.
    • sdk level 30 View.java 에는 다음의 코드가 없습니다.
    •  
 case R.styleable.View_clipToOutline:
                    setClipToOutline(a.getBoolean(attr, false));
                    break;

 

CustomView 에 outline Provider 를 set 하는 방법

CustomView 의 속성값에 radius 를 받도록 해서 생성자에서 CustomOutlineProvider 를 생성하여 set 해주는 방법이 있습니다.

 

장점 : 코드 내에서 extension 함수를 호출해서 view 마다 setCustomOutline 함수를 호출할 필요가 없습니다.

단점 : 필요에 따라 CustomView 를 여러개 만들어야 합니다.

 

Outline 을 재설정하는 rebuildOutline 이 호출되는 시점

 

rebuildOutline 이 호출되는 시점은 다음과 같습니다.

  1. invalidateOutline 내에서
  2. onAttachedToWindow 내에서
  3. setBackgroundBounds 내에서
  4. sizeChange 내에서
  5. invalidateDrawable 내에서

 

invalidateOutline 이 호출되는 시점 (View 내로 한정할 때)은 다음과 같습니다.

  1. setOutlineProvider 내에서
  2. setBackgroundDrawable 내에서
  3. internalSetPadding 내에서 (maxTargetSdk = Build.VERSION_CODES.P)

 

 

ClipChildren

ClipChildren 은 자식 view 가 부모 view 의 영역까지 사용하게 해주는 속성입니다.

내부 동작에 대해서 알아보겠습니다.

layout 의 cilpChidren 속성을 false 로 수정하면 내부적으로 setClipChildren 호출됩니다.

ViewGroup.java


    public void setClipChildren(boolean clipChildren) {
        boolean previousValue = (mGroupFlags & FLAG_CLIP_CHILDREN) == FLAG_CLIP_CHILDREN;
        if (clipChildren != previousValue) {
            setBooleanFlag(FLAG_CLIP_CHILDREN, clipChildren); 👈
            for (int i = 0; i < mChildrenCount; ++i) {
                View child = getChildAt(i);
                if (child.mRenderNode != null) {
                    child.mRenderNode.setClipToBounds(clipChildren);
                }
            }
            invalidate(true);
        }
    }

👈 FLAG_CLIP_CHILDREN 값이 false 로 바뀌게 됩니다.

View.java

    boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
        ...
        
        if (!drawingWithRenderNode) {
            // apply clips directly, since RenderNode won't do it for this draw
      👉    if ((parentFlags & ViewGroup.FLAG_CLIP_CHILDREN) != 0 && cache == null) {
                if (offsetForScroll) {
                    canvas.clipRect(sx, sy, sx + getWidth(), sy + getHeight());
                } else {
                    if (!scalingRequired || cache == null) {
                        canvas.clipRect(0, 0, getWidth(), getHeight());
                    } else {
                        canvas.clipRect(0, 0, cache.getWidth(), cache.getHeight());
                    }
                }
            }

👉 draw 함수 내에서 부모 속성이 FLAG_CLIP_CHILDREN 가 true 이면 clipRect 를 호출하며 canvas 를 자신의 영역만큼 canvas 를 clip합니다. 해당 값이 false 이기 때문에 canvas.clipRect 가 호출되지 않아 위 처럼 부모 canvas 영역을 그릴수 있게됩니다.

 

ClipToPadding

ClipToPadding 은 Padding 영역만큼 사용할 수 있게 해주는 속성입니다.

내부 동작에 대해서 알아보겠습니다.

clipToPadding 속성을 false 로 수정하면 내부적으로 setClipToPadding 이 호출됩니다.

 

ViewGroup.java

👉  protected static final int CLIP_TO_PADDING_MASK = FLAG_CLIP_TO_PADDING | FLAG_PADDING_NOT_NULL;
    
    ...

    public void setClipToPadding(boolean clipToPadding) {
        if (hasBooleanFlag(FLAG_CLIP_TO_PADDING) != clipToPadding) {
            setBooleanFlag(FLAG_CLIP_TO_PADDING, clipToPadding);
            invalidate(true);
        }
    }

👉 FLAG_CLIP_TO_PADDING 값은 멤버변수인 CLIP_TO_PADDING_MASK 로 사용됩니다. (padding 존재 여부 및 clipToPadding 값)

 

ViewGroup.java


    @Override
    protected void dispatchDraw(Canvas canvas) {
        ...
    
        int clipSaveCount = 0;
  👉    final boolean clipToPadding = (flags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK;
        if (clipToPadding) {
            clipSaveCount = canvas.save(Canvas.CLIP_SAVE_FLAG);
            canvas.clipRect(mScrollX + mPaddingLeft, mScrollY + mPaddingTop,
                    mScrollX + mRight - mLeft - mPaddingRight,
                    mScrollY + mBottom - mTop - mPaddingBottom);
        }

👉 CLIP_TO_PADDING_MASK 는 dispatchDraw 에서 체크 되고 clipToPadding 이면 padding 만큼 canvas 를 clip합니다.

728x90
반응형