드래그 앤 드롭 사용

Android 드래그 앤 드롭 프레임워크를 사용하면 앱에 대화형 드래그 앤 드롭 기능을 추가할 수 있습니다. 사용자는 드래그 앤 드롭을 통해 텍스트, 이미지, 객체 및 URI로 표현될 수 있는 모든 콘텐츠를 앱 내에서 한 View에서 다른 View로 또는 멀티 윈도우 모드의 앱 간에 복사하거나 이동할 수 있습니다.

앱 내에서 드래그 앤 드롭 중인 텍스트 문자열과 이미지 화면 분할 모드에서 앱 간에 드래그 앤 드롭 중인 텍스트 문자열과 이미지
그림 1. 앱 내에서 드래그 앤 드롭
그림 2. 앱 간의 드래그 앤 드롭

프레임워크에는 드래그 이벤트 클래스, 드래그 리스너, 도우미 클래스와 메서드가 포함됩니다. 기본적으로 데이터 전송을 가능하게 하도록 설계되었지만, 다른 UI 작업에 프레임워크를 사용할 수 있습니다. 예를 들어 사용자가 색상 아이콘을 다른 아이콘 위로 드래그하면 색상을 혼합하는 앱을 만들 수 있습니다. 하지만 이 문서의 나머지 부분에서는 데이터 전송과 관련하여 드래그 앤 드롭 프레임워크를 설명합니다.

개요

드래그 앤 드롭 작업은 사용자가 앱에서 데이터 드래그를 시작하는 신호로 인식하는 UI 동작을 할 때 시작됩니다. 이에 응답하여 앱은 시스템에 드래그 앤 드롭 작업이 시작되었음을 알립니다. 시스템은 드래그되는 데이터를 표현하기 위해 앱을 다시 호출합니다. 이를 드래그 섀도우라고 합니다.

사용자가 앱 레이아웃 위로 드래그 섀도우를 이동하면 시스템에서 레이아웃의 View 객체와 연결된 드래그 이벤트 리스너 및 콜백 메서드로 드래그 이벤트를 전송합니다. 사용자가 데이터를 허용할 수 있는 뷰 (드롭 타겟)에 드래그 섀도우를 놓으면 시스템은 데이터를 타겟으로 전송합니다. 드래그 앤 드롭 작업은 드래그 섀도우가 드롭 타겟 위에 있는지와 관계없이 사용자가 드래그 섀도우를 놓으면 종료됩니다.

View.OnDragListener를 구현하여 드래그 이벤트 리스너를 만듭니다. View 객체의 setOnDragListener() 메서드를 사용하여 드롭 타겟의 리스너를 설정합니다. 레이아웃의 각 뷰에는 onDragEvent() 콜백 메서드도 있습니다.

애플리케이션은 드래그 이벤트를 전송하도록 시스템에 지시하는 startDragAndDrop() 메서드를 호출하여 드래그 앤 드롭 작업을 시작하도록 시스템에 알립니다. 또한 이 메서드는 사용자가 드래그하는 데이터와 데이터를 설명하는 메타데이터를 시스템에 제공합니다. 현재 레이아웃의 모든 View에서 startDragAndDrop()를 호출할 수 있습니다. 시스템에서는 View 객체만 사용하여 레이아웃의 전역 설정에 액세스합니다.

드래그 앤 드롭 작업 중 시스템은 레이아웃에 있는 View 객체의 드래그 이벤트 리스너 또는 콜백 메서드로 드래그 이벤트를 전송합니다. 리스너와 콜백 메서드는 데이터를 드롭할 때 메타데이터를 사용하여 데이터 허용 여부를 결정합니다. 사용자가 드롭 타겟(데이터를 수락하는 View)에 데이터를 드롭하면 시스템은 데이터가 포함된 드래그 이벤트 객체를 드롭 타겟의 드래그 이벤트 리스너 또는 콜백 메서드로 전송합니다.

드래그 이벤트 리스너 및 콜백 메서드

ViewView.OnDragListener를 구현하는 드래그 이벤트 리스너 또는 뷰의 onDragEvent() 콜백 메서드를 사용하여 드래그 이벤트를 수신합니다. 시스템에서 메서드나 리스너를 호출할 때 DragEvent 인수를 제공합니다.

대부분의 경우 콜백 메서드보다 리스너를 사용하는 것이 더 좋습니다. UI를 설계할 때 일반적으로는 View 클래스의 서브클래스를 만들지 않지만, 콜백 메서드를 사용하면 서브클래스를 만들어 메서드를 재정의해야 합니다. 이와 달리 리스너는 하나의 리스너 클래스를 구현하여 여러 다른 View 객체에 사용할 수 있습니다. 익명의 인라인 클래스 또는 람다 표현식으로 이를 구현할 수도 있습니다. View 객체의 리스너를 설정하려면 setOnDragListener()를 호출하세요.

또는 메서드를 재정의하지 않고 onDragEvent()의 기본 구현을 변경할 수 있습니다. 뷰에서 OnReceiveContentListener를 설정합니다. 자세한 내용은 setOnReceiveContentListener()를 참고하세요. 그러면 onDragEvent() 메서드는 기본적으로 다음을 실행합니다.

  • startDragAndDrop() 호출에 대한 응답으로 true를 반환합니다.
  • 드래그 앤 드롭 데이터가 뷰에 드롭되면 performReceiveContent()를 호출합니다. 데이터는 ContentInfo 객체로 메서드에 전달됩니다. 이 메서드는 OnReceiveContentListener를 호출합니다.

  • 드래그 앤 드롭 데이터가 뷰에 드롭되고 OnReceiveContentListener가 콘텐츠를 사용하면 true를 반환합니다.

특히 앱에 맞게 데이터를 처리하도록 OnReceiveContentListener를 정의합니다. API 수준 24까지의 이전 버전과의 호환성을 위해 Jetpack 버전의 OnReceiveContentListener를 사용하세요.

드래그 이벤트 리스너와 View 객체의 콜백 메서드를 둘 수 있습니다. 이 경우 시스템에서 리스너를 먼저 호출합니다. 리스너가 false를 반환하지 않는 한, 시스템은 콜백 메서드를 호출하지 않습니다.

onDragEvent() 메서드와 View.OnDragListener의 조합은 터치 이벤트에 사용되는 onTouchEvent()View.OnTouchListener의 조합과 유사합니다.

드래그 앤 드롭 처리 과정

드래그 앤 드롭 프로세스에는 시작, 계속, 드롭, 종료됨과 같은 4가지 단계 또는 상태가 있습니다.

시작됨

사용자의 드래그 동작에 응답하여 애플리케이션은 startDragAndDrop()를 호출하여 드래그 앤 드롭 작업을 시작하도록 시스템에 알립니다. 메서드의 인수는 다음을 제공합니다.

  • 드래그할 데이터입니다.
  • 드래그 섀도우를 그리는 콜백
  • 드래그된 데이터를 설명하는 메타데이터: 시스템은 애플리케이션을 다시 호출하여 드래그 섀도우를 가져오는 방식으로 응답합니다. 그런 다음 기기에 드래그 섀도우를 표시합니다. 다음으로, 시스템은 작업 유형이 ACTION_DRAG_STARTED인 드래그 이벤트를 현재 레이아웃에 있는 모든 View 객체의 드래그 이벤트 리스너로 보냅니다. 가능한 드롭 이벤트를 포함하여 드래그 이벤트를 계속 수신하려면 드래그 이벤트 리스너가 true를 반환해야 합니다. 그러면 리스너가 시스템에 등록됩니다. 등록된 리스너만 드래그 이벤트를 계속 수신합니다. 이 시점에서 리스너는 뷰가 드롭 이벤트를 수락할 수 있음을 표시하기 위해 드롭 타겟 View 객체의 모양을 변경할 수도 있습니다. 드래그 이벤트 리스너가 false를 반환하면 시스템에서 작업 유형이 ACTION_DRAG_ENDED인 드래그 이벤트를 전송할 때까지 현재 작업의 드래그 이벤트를 수신하지 않습니다. false를 반환하면 리스너는 드래그 앤 드롭 작업에 관심이 없으며 드래그된 데이터를 수락하지 않겠다고 시스템에 알립니다.
진행 중
사용자가 드래그를 계속합니다. 드래그 섀도우가 드롭 타겟의 경계 상자를 교차하면 시스템에서 하나 이상의 드래그 이벤트를 타겟의 드래그 이벤트 리스너로 전송합니다. 리스너는 이벤트에 대한 응답으로 드롭 타겟 View의 모양을 변경할 수 있습니다. 예를 들어 이벤트에서 드래그 섀도우가 드롭 타겟의 경계 상자에 진입한다고 표시하면(작업 유형 ACTION_DRAG_ENTERED) 리스너는 View를 강조표시하여 반응할 수 있습니다.
드롭됨
사용자가 드롭 타겟의 경계 상자 내에 드래그 섀도우를 놓습니다. 시스템은 드롭 타겟의 리스너에 작업 유형이 ACTION_DROP인 드래그 이벤트를 보냅니다. 드래그 이벤트 객체에는 작업을 시작하는 startDragAndDrop() 호출로 시스템에 전달되는 데이터가 포함됩니다. 리스너가 드롭된 데이터를 성공적으로 처리하면 리스너는 시스템에 불리언 true를 반환해야 합니다. 이 단계는 리스너가 드래그 이벤트(드롭 타겟)를 수신하도록 등록된 View의 경계 상자 내에서 사용자가 드래그 섀도우를 드롭하는 경우에만 발생합니다. 사용자가 그 외에 다른 상황에서 드래그 섀도우를 놓으면 ACTION_DROP 드래그 이벤트가 전송되지 않습니다.
종료됨

사용자가 드래그 섀도우를 놓고 시스템이

작업 유형이 ACTION_DROP인 드래그 이벤트를 전송하면 필요한 경우 시스템에서 작업 유형이 ACTION_DRAG_ENDED인 드래그 이벤트를 전송하여 드래그 앤 드롭 작업이 종료되었음을 나타냅니다. 이 동작은 사용자가 드래그 섀도우를 놓는 위치와 관계없이 이루어집니다. 이 이벤트는 리스너가 ACTION_DROP 이벤트를 수신하더라도 드래그 이벤트를 수신하도록 등록된 모든 리스너에 전송됩니다.

각 단계는 드래그 앤 드롭 작업 섹션에 자세히 설명되어 있습니다.

드래그 이벤트

시스템은 DragEvent 객체 형태로 드래그 이벤트를 전송합니다. 이 객체에는 드래그 앤 드롭 프로세스에서 발생하는 상황을 설명하는 작업 유형이 포함되어 있습니다. 작업 유형에 따라 객체는 다른 데이터를 포함할 수도 있습니다.

드래그 이벤트 리스너는 DragEvent 객체를 수신합니다. 작업 유형을 가져오려면 리스너가 DragEvent.getAction()을 호출합니다. 표 1에 설명된 DragEvent 클래스의 상수로 정의된 6개의 값을 사용할 수 있습니다.

표 1. DragEvent 작업 유형

작업 유형 의미
ACTION_DRAG_STARTED 애플리케이션이 startDragAndDrop()를 호출하고 드래그 섀도우를 가져옵니다. 리스너에서 이 작업의 드래그 이벤트를 계속 수신하려면 불리언 true를 시스템에 반환해야 합니다.
ACTION_DRAG_ENTERED 드래그 섀도우는 드래그 이벤트 리스너의 View 경계 상자에 들어갑니다. 이는 드래그 섀도우가 경계 상자에 진입할 때 리스너가 수신하는 첫 번째 이벤트 작업 유형입니다.
ACTION_DRAG_LOCATION ACTION_DRAG_ENTERED 이벤트 후에 드래그 섀도우는 여전히 드래그 이벤트 리스너의 View 경계 상자 내에 있습니다.
ACTION_DRAG_EXITED ACTION_DRAG_ENTERED 및 하나 이상의 ACTION_DRAG_LOCATION 이벤트 후에 드래그 섀도우는 드래그 이벤트 리스너의 View 경계 상자 밖으로 이동합니다.
ACTION_DROP 드래그 이벤트 리스너의 View 위로 드래그 그림자가 놓입니다. 이 작업 유형은 리스너가 ACTION_DRAG_STARTED 드래그 이벤트에 대한 응답으로 불리언 true를 반환하는 경우에만 View 객체의 리스너로 전송됩니다. 이 작업 유형은 사용자가 리스너가 등록되지 않은 View 위에 드래그 섀도우를 놓거나 현재 레이아웃에 속하지 않은 항목 위에 드래그 섀도우를 놓는 경우에는 전송되지 않습니다.

드롭이 성공적으로 처리되면 리스너는 불리언 true를 반환합니다. 그렇지 않으면 false를 반환해야 합니다.

ACTION_DRAG_ENDED 시스템이 드래그 앤 드롭 작업을 종료하고 있습니다. 이 작업 유형이 반드시 ACTION_DROP 이벤트 다음에 발생하는 것은 아닙니다. 시스템에서 ACTION_DROP를 전송하면 ACTION_DRAG_ENDED 작업 유형을 수신한다고 해서 드롭이 성공했음을 의미하지는 않습니다. 리스너에서는 표 2와 같이 getResult()를 호출하여 ACTION_DROP에 대한 응답으로 반환되는 값을 가져와야 합니다. ACTION_DROP 이벤트가 전송되지 않으면 getResult()false를 반환합니다.

DragEvent 객체에는 애플리케이션이 startDragAndDrop() 호출에서 시스템에 제공하는 데이터와 메타데이터도 포함됩니다. 일부 데이터는 표 2에 요약된 특정 작업 유형에만 유효합니다. 이벤트 및 이벤트 관련 데이터에 관한 자세한 내용은 드래그 앤 드롭 작업 섹션을 참고하세요.

표 2. 작업 유형별 유효한 DragEvent 데이터

getAction()
getClipDescription()
getLocalState()
getX()
getY()
getClipData()
getResult()
ACTION_DRAG_STARTED ✓ ✓ ✓ ✓    
ACTION_DRAG_ENTERED ✓ ✓        
ACTION_DRAG_LOCATION ✓ ✓ ✓ ✓    
ACTION_DRAG_EXITED ✓ ✓        
ACTION_DROP ✓ ✓ ✓ ✓ ✓  
ACTION_DRAG_ENDED   ✓       ✓

DragEvent 메서드 getAction(), describeContents(), writeToParcel(), toString()는 항상 유효한 데이터를 반환합니다.

메서드가 특정 작업 유형에 유효한 데이터를 포함하지 않으면 결과 유형에 따라 null 또는 0을 반환합니다.

드래그 섀도우

드래그 앤 드롭 작업 중 시스템은 사용자가 드래그하는 이미지를 표시합니다. 데이터 이동 작업에서 이 이미지는 드래그되고 있는 데이터를 나타냅니다. 다른 작업의 경우 이미지는 드래그 작업의 특정 측면을 나타냅니다.

이 이미지를 드래그 그림자라고 합니다. View.DragShadowBuilder 객체에 관해 선언하는 메서드를 사용하여 객체를 만듭니다. startDragAndDrop()를 사용하여 드래그 앤 드롭 작업을 시작할 때 시스템에 빌더를 전달합니다. startDragAndDrop()에 대한 응답의 일부로 시스템은 View.DragShadowBuilder에 정의된 콜백 메서드를 호출하여 드래그 섀도우를 가져옵니다.

View.DragShadowBuilder 클래스에는 다음과 같은 두 가지 생성자가 있습니다.

View.DragShadowBuilder(View)

이 생성자는 애플리케이션의 모든 View 객체를 허용합니다. 생성자는 View 객체를 View.DragShadowBuilder 객체에 저장하므로 콜백은 이 객체에 액세스하여 드래그 섀도우를 생성할 수 있습니다. 뷰는 사용자가 드래그 작업을 시작하기 위해 선택한 View일 필요는 없습니다.

이 생성자를 사용하면 View.DragShadowBuilder를 확장하거나 빌더의 메서드를 재정의할 필요가 없습니다. 기본적으로 드래그 섀도우는 인수로 전달하는 View와 동일한 모양이며 사용자가 화면을 터치하는 위치 중앙에 배치됩니다.

View.DragShadowBuilder()

이 생성자를 사용하면 View.DragShadowBuilder 객체에서 View 객체를 사용할 수 없습니다. 필드는 null로 설정됩니다. View.DragShadowBuilder를 확장하고 그 메서드를 재정의해야 합니다. 그러지 않으면 드래그 그림자가 보이지 않습니다. 시스템은 오류를 발생시키지 않습니다.

View.DragShadowBuilder 클래스에는 함께 드래그 그림자를 만드는 두 가지 메서드가 있습니다.

onProvideShadowMetrics()

애플리케이션이 startDragAndDrop()을 호출한 직후 시스템은 이 메서드를 호출합니다. 메서드를 사용하여 드래그 섀도우의 크기와 터치 포인트를 시스템에 전송합니다. 이 메서드에는 다음과 같은 두 개의 매개변수가 있습니다.

outShadowSize: Point 객체입니다. 드래그 그림자 너비는 x에 해당하고 높이는 y에 해당합니다.

outShadowTouchPoint: Point 객체입니다. 터치 포인트는 드래그 섀도우 내 위치로, 드래그하는 동안 사용자 손가락 아래에 있어야 합니다. X 위치는 x에 있고 Y 위치는 y에 해당합니다.

onDrawShadow()

onProvideShadowMetrics() 호출 직후, 시스템은 onDrawShadow()를 호출하여 드래그 섀도우를 만듭니다. 이 메서드에는 단일 인수인 Canvas 객체가 있으며 이 객체는 onProvideShadowMetrics()에 제공된 매개변수로 시스템에서 구성합니다. 이 메서드는 제공된 Canvas에 드래그 섀도우를 그립니다.

성능을 개선하려면 드래그 섀도우의 크기를 작게 유지합니다. 단일 항목의 경우 아이콘을 사용할 수 있습니다. 여러 항목을 선택하는 경우 화면 전체에 걸쳐 전체 이미지를 표시하기보다는 스택 형태로 아이콘을 사용할 수 있습니다.

드래그 앤 드롭 작업

이 섹션에서는 드래그를 시작하고, 드래그 중에 이벤트에 응답하고, 드롭 이벤트에 응답하고, 드래그 앤 드롭 작업을 종료하는 방법을 단계별로 보여줍니다.

드래그 시작

사용자는 View 객체에서 드래그 동작(일반적으로 길게 터치)을 사용하여 드래그를 시작합니다. 이에 대한 응답으로 앱은 다음을 실행해야 합니다.

  1. 이동되는 데이터에 맞는 ClipData 객체와 ClipData.Item 객체를 만듭니다. ClipData의 일환으로 ClipData 내의 ClipDescription 객체에 저장된 메타데이터를 제공합니다. 데이터 이동을 나타내지 않는 드래그 앤 드롭 작업의 경우 실제 객체 대신 null를 사용할 수 있습니다.

    예를 들어, 이 코드 스니펫은 ImageView의 태그(또는 라벨)가 포함된 ClipData 객체를 생성하여 ImageView의 길게 터치 동작에 응답하는 방법을 보여줍니다.

    Kotlin

    // Create a string for the ImageView label.
    val IMAGEVIEW_TAG = "icon bitmap"
    ...
    val imageView = ImageView(context).apply {
    // Set the bitmap for the ImageView from an icon bitmap defined elsewhere.
    setImageBitmap(iconBitmap)
    tag = IMAGEVIEW_TAG
    setOnLongClickListener { v ->
            // Create a new ClipData. This is done in two steps to provide
            // clarity. The convenience method ClipData.newPlainText() can
            // create a plain text ClipData in one step.
    
            // Create a new ClipData.Item from the ImageView object's tag.
            val item = ClipData.Item(v.tag as? CharSequence)
    
            // Create a new ClipData using the tag as a label, the plain text
            // MIME type, and the already-created item. This creates a new
            // ClipDescription object within the ClipData and sets its MIME type
            // to "text/plain".
            val dragData = ClipData(
                v.tag as? CharSequence,
                arrayOf(ClipDescription.MIMETYPE_TEXT_PLAIN),
                item)
    
            // Instantiate the drag shadow builder.
            val myShadow = MyDragShadowBuilder(view: this)
    
            // Start the drag.
            v.startDragAndDrop(dragData,  // The data to be dragged.
                               myShadow,  // The drag shadow builder.
                               null,      // No need to use local data.
                               0          // Flags. Not currently used, set to 0.
            )
    
           // Indicate that the long-click is handled.
           true
    }
    }
    

    Java

    // Create a string for the ImageView label.
    private static final String IMAGEVIEW_TAG = "icon bitmap";
    ...
    // Create a new ImageView.
    ImageView imageView = new ImageView(context);
    
    // Set the bitmap for the ImageView from an icon bitmap defined elsewhere.
    imageView.setImageBitmap(iconBitmap);
    
    // Set the tag.
    imageView.setTag(IMAGEVIEW_TAG);
    
    // Set a long-click listener for the ImageView using an anonymous listener
    // object that implements the OnLongClickListener interface.
    imageView.setOnLongClickListener( v -> {
    
    // Create a new ClipData. This is done in two steps to provide clarity. The
    // convenience method ClipData.newPlainText() can create a plain text
    // ClipData in one step.
    
    // Create a new ClipData.Item from the ImageView object's tag.
    ClipData.Item item = new ClipData.Item((CharSequence) v.getTag());
    
    // Create a new ClipData using the tag as a label, the plain text MIME type,
    // and the already-created item. This creates a new ClipDescription object
    // within the ClipData and sets its MIME type to "text/plain".
    ClipData dragData = new ClipData(
            (CharSequence) v.getTag(),
            new String[] { ClipDescription.MIMETYPE_TEXT_PLAIN },
            item);
    
    // Instantiate the drag shadow builder.
    View.DragShadowBuilder myShadow = new MyDragShadowBuilder(imageView);
    
    // Start the drag.
    v.startDragAndDrop(dragData,  // The data to be dragged.
                           myShadow,  // The drag shadow builder.
                           null,      // No need to use local data.
                           0          // Flags. Not currently used, set to 0.
    );
    
    // Indicate that the long-click is handled.
    return true;
    });
    
  2. View.DragShadowBuilder의 메서드를 재정의하여 myDragShadowBuilder를 정의합니다. 다음 코드 스니펫은 TextView에 관한 작은 직사각형 회색 드래그 그림자를 만듭니다.

    Kotlin

    private class MyDragShadowBuilder(view: View) : View.DragShadowBuilder(view) {
    
    private val shadow = ColorDrawable(Color.LTGRAY)
    
    // Define a callback that sends the drag shadow dimensions and touch point
    // back to the system.
    override fun onProvideShadowMetrics(size: Point, touch: Point) {
    
            // Set the width of the shadow to half the width of the original
            // View.
            val width: Int = view.width / 2
    
            // Set the height of the shadow to half the height of the original
            // View.
            val height: Int = view.height / 2
    
            // The drag shadow is a ColorDrawable. Set its dimensions to
            // be the same as the Canvas that the system provides. As a result,
            // the drag shadow fills the Canvas.
            shadow.setBounds(0, 0, width, height)
    
            // Set the size parameter's width and height values. These get back
            // to the system through the size parameter.
            size.set(width, height)
    
            // Set the touch point's position to be in the middle of the drag
            // shadow.
            touch.set(width / 2, height / 2)
    }
    
    // Define a callback that draws the drag shadow in a Canvas that the system
    // constructs from the dimensions passed to onProvideShadowMetrics().
    override fun onDrawShadow(canvas: Canvas) {
    
            // Draw the ColorDrawable on the Canvas passed in from the system.
            shadow.draw(canvas)
    }
    }
    

    Java

    private static class MyDragShadowBuilder extends View.DragShadowBuilder {
    
    // The drag shadow image, defined as a drawable object.
    private static Drawable shadow;
    
    // Constructor.
    public MyDragShadowBuilder(View view) {
    
            // Store the View parameter.
            super(view);
    
            // Create a draggable image that fills the Canvas provided by the
            // system.
            shadow = new ColorDrawable(Color.LTGRAY);
    }
    
    // Define a callback that sends the drag shadow dimensions and touch point
    // back to the system.
    @Override
    public void onProvideShadowMetrics (Point size, Point touch) {
    
            // Define local variables.
            int width, height;
    
            // Set the width of the shadow to half the width of the original
            // View.
            width = getView().getWidth() / 2;
    
            // Set the height of the shadow to half the height of the original
            // View.
            height = getView().getHeight() / 2;
    
            // The drag shadow is a ColorDrawable. Set its dimensions to
            // be the same as the Canvas that the system provides. As a result,
            // the drag shadow fills the Canvas.
            shadow.setBounds(0, 0, width, height);
    
            // Set the size parameter's width and height values. These get back
            // to the system through the size parameter.
            size.set(width, height);
    
            // Set the touch point's position to be in the middle of the drag
            // shadow.
            touch.set(width / 2, height / 2);
    }
    
    // Define a callback that draws the drag shadow in a Canvas that the system
    // constructs from the dimensions passed to onProvideShadowMetrics().
    @Override
    public void onDrawShadow(Canvas canvas) {
    
            // Draw the ColorDrawable on the Canvas passed in from the system.
            shadow.draw(canvas);
    }
    }
    

드래그 시작에 응답

드래그 작업 중 시스템은 드래그 이벤트를 현재 레이아웃에 포함된 View 객체의 드래그 이벤트 리스너로 전송합니다. 리스너는 이에 반응하여 DragEvent.getAction()를 호출하여 작업 유형을 가져옵니다. 드래그 시작 시 이 메서드는 ACTION_DRAG_STARTED를 반환합니다.

작업 유형이 ACTION_DRAG_STARTED인 이벤트에 대한 응답으로 드래그 이벤트 리스너는 다음을 실행해야 합니다.

  1. DragEvent.getClipDescription()를 호출하고 반환된 ClipDescription에서 MIME 유형 메서드를 사용하여 리스너가 드래그되는 데이터를 수락할 수 있는지 확인합니다.

    드래그 앤 드롭 작업이 데이터 이동을 나타내지 않으면 이 작업이 필요하지 않을 수 있습니다.

  2. 드래그 이벤트 리스너가 드롭을 수락할 수 있는 경우 true를 반환하여 드래그 이벤트를 리스너로 계속 전송하도록 시스템에 알려야 합니다. 리스너가 드롭을 수락할 수 없으면 리스너는 false를 반환해야 합니다. 그러면 시스템에서 ACTION_DRAG_ENDED를 전송하여 드래그 앤 드롭 작업을 종료할 때까지 리스너로 드래그 이벤트 전송을 중지합니다.

ACTION_DRAG_STARTED 이벤트의 경우 getClipData(), getX(), getY(), getResult()과 같은 DragEvent 메서드는 유효하지 않습니다.

드래그 중 이벤트 처리

드래그 작업 중에 ACTION_DRAG_STARTED 드래그 이벤트에 응답하여 true를 반환하는 드래그 이벤트 리스너는 드래그 이벤트를 계속 수신합니다. 드래그 중에 리스너가 수신하는 드래그 이벤트 유형은 드래그 섀도우의 위치와 리스너의 View의 표시 여부에 따라 다릅니다. 리스너는 주로 드래그 이벤트를 사용하여 View의 모양을 변경해야 하는지 결정합니다.

드래그 작업 중, DragEvent.getAction()은 다음 세 가지 값 중 하나를 반환합니다.

  • ACTION_DRAG_ENTERED: 터치 포인트(화면에서 사용자의 손가락이나 마우스 아래에 있는 지점)가 리스너의 View의 경계 상자에 들어가면 리스너가 이 이벤트 작업 유형을 수신합니다.
  • ACTION_DRAG_LOCATION: 리스너가 ACTION_DRAG_ENTERED 이벤트를 수신하면 ACTION_DRAG_EXITED 이벤트를 수신할 때까지 터치 포인트가 움직일 때마다 새 ACTION_DRAG_LOCATION 이벤트를 수신합니다. getX()getY() 메서드는 터치 포인트의 X 및 Y 좌표를 반환합니다.
  • ACTION_DRAG_EXITED: 이 이벤트 작업 유형은 이전에 ACTION_DRAG_ENTERED를 수신하는 리스너로 전송됩니다. 이 이벤트는 드래그 섀도우 터치 포인트가 리스너 View의 경계 상자 내부에서 외부로 이동할 때 전송됩니다.

드래그 이벤트 리스너는 이러한 작업 유형에 반응하지 않아도 됩니다. 리스너가 시스템에 값을 반환하면 값이 무시됩니다.

다음은 이러한 작업 유형 각각에 응답하는 데 관한 몇 가지 가이드라인입니다.

  • 리스너는 ACTION_DRAG_ENTERED 또는 ACTION_DRAG_LOCATION의 응답으로 뷰가 잠재적 드롭 타겟임을 나타내는 View의 모양을 변경할 수 있습니다.
  • 작업 유형이 ACTION_DRAG_LOCATION인 이벤트에는 터치 포인트의 위치에 상응하는 getX()getY()의 유효한 데이터가 포함됩니다. 리스너는 이 정보를 사용하여 터치 포인트의 View 모양을 변경하거나 사용자가 드래그 섀도우를 놓을 수 있는 정확한 위치, 즉 데이터를 드롭할 수 있습니다.
  • ACTION_DRAG_EXITED에 응답하여 리스너는 ACTION_DRAG_ENTERED 또는 ACTION_DRAG_LOCATION에 대한 응답으로 적용되는 모든 모양 변경사항을 재설정해야 합니다. 이는 View가 곧 드롭이 일어날 수 있는 드롭 타겟이 아님을 사용자에게 나타냅니다.

드롭에 응답

사용자가 View 위에 드래그 섀도우를 놓고 View에서 이전에 드래그되는 콘텐츠를 허용할 수 있다고 보고하면 시스템은 작업 유형이 ACTION_DROPView에 드래그 이벤트를 전달합니다.

드래그 이벤트 리스너는 다음을 실행해야 합니다.

  1. getClipData()를 호출하여 startDragAndDrop() 호출 시 원래 제공된 ClipData 객체를 가져오고 데이터를 처리합니다. 드래그 앤 드롭 작업이 데이터 이동을 나타내지 않으면 필요하지 않습니다.

  2. 불리언 true를 반환하여 드롭이 성공적으로 처리되었음을 나타내거나 그렇지 않은 경우 false를 반환합니다. 반환된 값은 최종 ACTION_DRAG_ENDED 이벤트에 관해 getResult()에서 반환한 값이 됩니다. 시스템이 ACTION_DROP 이벤트를 전송하지 않으면 ACTION_DRAG_ENDED 이벤트에 관해 getResult()에서 반환하는 값은 false입니다.

ACTION_DROP 이벤트의 경우 getX()getY()는 드롭을 수신하는 View의 좌표계를 사용하여 드롭 순간 터치 포인트의 XY 위치를 반환합니다.

시스템에서는 사용자가 드래그 이벤트 리스너가 드래그 이벤트를 수신하지 않는 View 위에 드래그 섀도우를 놓을 수 있습니다. 또한 사용자가 애플리케이션 UI의 빈 영역이나 애플리케이션 외부의 영역에 드래그 섀도우를 놓을 수 있습니다. 이러한 경우 모두 시스템은 ACTION_DRAG_ENDED 이벤트를 전송하지만 작업 유형이 ACTION_DROP인 이벤트를 전송하지 않습니다.

드래그 종료에 응답

사용자가 드래그 섀도우를 놓으면 즉시 시스템에서 작업 유형이 ACTION_DRAG_ENDED인 드래그 이벤트를 애플리케이션의 모든 드래그 이벤트 리스너로 전송합니다. 이는 드래그 앤 드롭 작업이 종료되었음을 나타냅니다.

각 드래그 이벤트 리스너는 다음을 실행해야 합니다.

  1. 작업 중에 리스너가 View 객체의 모양을 변경하면 리스너는 View를 기본 모양으로 재설정해야 합니다. 이는 사용자에게 작업이 종료되었음을 알려주는 시각적 표시입니다.
  2. 리스너에서 선택적으로 getResult()를 호출하여 작업에 관한 자세한 정보를 확인할 수 있습니다. 리스너가 작업 유형이 ACTION_DROP인 이벤트에 대한 응답으로 true를 반환하면 getResult()는 불리언 true를 반환합니다. 그 외 모든 경우 getResult()는 시스템에서 ACTION_DROP 이벤트를 전송하지 않는 경우를 포함하여 불리언 false을 반환합니다.
  3. 드래그 앤 드롭 작업이 성공적으로 완료되었음을 나타내려면 리스너가 불리언 true를 시스템에 반환해야 합니다.

드래그 이벤트에 응답: 예

모든 드래그 이벤트는 드래그 이벤트 메서드나 리스너에서 수신합니다. 다음 코드 스니펫은 드래그 이벤트에 응답하는 간단한 예입니다.

Kotlin

val imageView = ImageView(this)

// Set the drag event listener for the View.
imageView.setOnDragListener { v, e ->

    // Handle each of the expected events.
    when (e.action) {
        DragEvent.ACTION_DRAG_STARTED -> {
            // Determine whether this View can accept the dragged data.
            if (e.clipDescription.hasMimeType(ClipDescription.MIMETYPE_TEXT_PLAIN)) {
                // As an example, apply a blue color tint to the View to
                // indicate that it can accept data.
                (v as? ImageView)?.setColorFilter(Color.BLUE)

                // Invalidate the view to force a redraw in the new tint.
                v.invalidate()

                // Return true to indicate that the View can accept the dragged
                // data.
                true
            } else {
                // Return false to indicate that, during the current drag and
                // drop operation, this View doesn't receive events again until
                // ACTION_DRAG_ENDED is sent.
                false
            }
        }
        DragEvent.ACTION_DRAG_ENTERED -> {
            // Apply a green tint to the View.
            (v as? ImageView)?.setColorFilter(Color.GREEN)

            // Invalidate the view to force a redraw in the new tint.
            v.invalidate()

            // Return true. The value is ignored.
            true
        }

        DragEvent.ACTION_DRAG_LOCATION ->
            // Ignore the event.
            true
        DragEvent.ACTION_DRAG_EXITED -> {
            // Reset the color tint to blue.
            (v as? ImageView)?.setColorFilter(Color.BLUE)

            // Invalidate the view to force a redraw in the new tint.
            v.invalidate()

            // Return true. The value is ignored.
            true
        }
        DragEvent.ACTION_DROP -> {
            // Get the item containing the dragged data.
            val item: ClipData.Item = e.clipData.getItemAt(0)

            // Get the text data from the item.
            val dragData = item.text

            // Display a message containing the dragged data.
            Toast.makeText(this, "Dragged data is $dragData", Toast.LENGTH_LONG).show()

            // Turn off color tints.
            (v as? ImageView)?.clearColorFilter()

            // Invalidate the view to force a redraw.
            v.invalidate()

            // Return true. DragEvent.getResult() returns true.
            true
        }

        DragEvent.ACTION_DRAG_ENDED -> {
            // Turn off color tinting.
            (v as? ImageView)?.clearColorFilter()

            // Invalidate the view to force a redraw.
            v.invalidate()

            // Do a getResult() and display what happens.
            when(e.result) {
                true ->
                    Toast.makeText(this, "The drop was handled.", Toast.LENGTH_LONG)
                else ->
                    Toast.makeText(this, "The drop didn't work.", Toast.LENGTH_LONG)
            }.show()

            // Return true. The value is ignored.
            true
        }
        else -> {
            // An unknown action type is received.
            Log.e("DragDrop Example", "Unknown action type received by View.OnDragListener.")
            false
        }
    }
}

Java

View imageView = new ImageView(this);

// Set the drag event listener for the View.
imageView.setOnDragListener( (v, e) -> {

    // Handle each of the expected events.
    switch(e.getAction()) {

        case DragEvent.ACTION_DRAG_STARTED:

            // Determine whether this View can accept the dragged data.
            if (e.getClipDescription().hasMimeType(ClipDescription.MIMETYPE_TEXT_PLAIN)) {

                // As an example, apply a blue color tint to the View to
                // indicate that it can accept data.
                ((ImageView)v).setColorFilter(Color.BLUE);

                // Invalidate the view to force a redraw in the new tint.
                v.invalidate();

                // Return true to indicate that the View can accept the dragged
                // data.
                return true;

            }

            // Return false to indicate that, during the current drag and drop
            // operation, this View doesn't receive events again until
            // ACTION_DRAG_ENDED is sent.
            return false;

        case DragEvent.ACTION_DRAG_ENTERED:

            // Apply a green tint to the View.
            ((ImageView)v).setColorFilter(Color.GREEN);

            // Invalidate the view to force a redraw in the new tint.
            v.invalidate();

            // Return true. The value is ignored.
            return true;

        case DragEvent.ACTION_DRAG_LOCATION:

            // Ignore the event.
            return true;

        case DragEvent.ACTION_DRAG_EXITED:

            // Reset the color tint to blue.
            ((ImageView)v).setColorFilter(Color.BLUE);

            // Invalidate the view to force a redraw in the new tint.
            v.invalidate();

            // Return true. The value is ignored.
            return true;

        case DragEvent.ACTION_DROP:

            // Get the item containing the dragged data.
            ClipData.Item item = e.getClipData().getItemAt(0);

            // Get the text data from the item.
            CharSequence dragData = item.getText();

            // Display a message containing the dragged data.
            Toast.makeText(this, "Dragged data is " + dragData, Toast.LENGTH_LONG).show();

            // Turn off color tints.
            ((ImageView)v).clearColorFilter();

            // Invalidate the view to force a redraw.
            v.invalidate();

            // Return true. DragEvent.getResult() returns true.
            return true;

        case DragEvent.ACTION_DRAG_ENDED:

            // Turn off color tinting.
            ((ImageView)v).clearColorFilter();

            // Invalidate the view to force a redraw.
            v.invalidate();

            // Do a getResult() and displays what happens.
            if (e.getResult()) {
                Toast.makeText(this, "The drop was handled.", Toast.LENGTH_LONG).show();
            } else {
                Toast.makeText(this, "The drop didn't work.", Toast.LENGTH_LONG).show();
            }

            // Return true. The value is ignored.
            return true;

        // An unknown action type is received.
        default:
            Log.e("DragDrop Example","Unknown action type received by View.OnDragListener.");
            break;
    }

    return false;

});

멀티 윈도우 모드에서 드래그 앤 드롭

Android 7.0 (API 수준 24) 이상을 실행하는 기기는 멀티 윈도우 모드를 지원합니다. 이 모드를 사용하면 사용자가 드래그 앤 드롭 작업을 사용하여 한 앱에서 다른 앱으로 데이터를 이동할 수 있습니다. 자세한 내용은 멀티 윈도우 지원을 참고하세요.

드래그 앤 드롭 작업이 시작되는 소스 앱에서 데이터를 제공합니다. 드래그 앤 드롭 작업이 종료되는 타겟 앱에서 데이터를 수신합니다.

드래그 앤 드롭 작업을 시작할 때 소스 앱은 DRAG_FLAG_GLOBAL 플래그를 설정하여 사용자가 다른 앱으로 데이터를 드래그할 수 있음을 표시해야 합니다.

데이터는 앱 경계를 넘어 이동하므로 앱은 콘텐츠 URI를 사용하여 데이터 액세스를 공유합니다. 이를 위해서는 다음이 필요합니다.

  • 소스 앱은 소스 앱이 타겟 앱에 부여하려는 데이터에 관한 읽기 또는 쓰기 액세스 권한에 따라 DRAG_FLAG_GLOBAL_URI_READ 플래그와 DRAG_FLAG_GLOBAL_URI_WRITE 플래그 중 하나 또는 둘 다를 설정해야 합니다.
  • 타겟 앱은 사용자가 앱으로 드래그하는 데이터를 처리하기 직전에 requestDragAndDropPermissions()를 호출해야 합니다. 타겟 앱이 더 이상 드래그 앤 드롭 데이터에 액세스할 필요가 없다면 앱은 requestDragAndDropPermissions()에서 반환된 객체에서 release()를 호출하면 됩니다. 그렇게 하지 않으면 포함하는 활동이 소멸될 때 권한이 해제됩니다. 구현에서 삭제된 URI를 처리하기 위해 새 활동을 시작하는 경우 새 활동에 동일한 권한을 부여해야 합니다. 클립 데이터와 플래그를 설정해야 합니다.

    Kotlin

    intent.setClipData(clipData)
    intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
    

    Java

    intent.setClipData(clipData);
    intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
    

다음 코드 스니펫은 드래그 앤 드롭 작업이 실행된 직후 드래그 앤 드롭 데이터에 대한 읽기 전용 액세스 권한을 해제하는 방법을 보여줍니다. 전체 예는 GitHub의 DragAndDrop 샘플을 참고하세요.

소스 드래그 앤 드롭 활동

Kotlin

// Drag a file stored in an images/ directory in internal storage.
val internalImagesDir = File(context.filesDir, "images")
val imageFile = File(internalImagesDir, imageFilename)
val uri = FileProvider.getUriForFile(context, contentAuthority, imageFile)

val listener = OnDragStartListener@{ view: View, _: DragStartHelper ->
    val clipData = ClipData(ClipDescription("Image Description",
                                            arrayOf("image/*")),
                            ClipData.Item(uri))
    // Must include DRAG_FLAG_GLOBAL to permit dragging data between apps.
    // This example provides read-only access to the data.
    val flags = View.DRAG_FLAG_GLOBAL or View.DRAG_FLAG_GLOBAL_URI_READ
    return@OnDragStartListener view.startDragAndDrop(clipData,
                                                     View.DragShadowBuilder(view),
                                                     null,
                                                     flags)
}

// Container where the image originally appears in the source app.
val srcImageView = findViewById<ImageView>(R.id.imageView)

// Detect and start the drag event.
DragStartHelper(srcImageView, listener).apply {
    attach()
}

Java

// Drag a file stored in an images/ directory in internal storage.
File internalImagesDir = new File(context.getFilesDir(), "images");
File imageFile = new File(internalImagesDir, imageFilename);
final Uri uri = FileProvider.getUriForFile(context, contentAuthority, imageFile);

// Container where the image originally appears in the source app.
ImageView srcImageView = findViewById(R.id.imageView);

// Enable the view to detect and start the drag event.
new DragStartHelper(srcImageView, (view, helper) -> {
    ClipData clipData = new ClipData(new ClipDescription("Image Description",
                                                          new String[] {"image/*"}),
                                     new ClipData.Item(uri));
    // Must include DRAG_FLAG_GLOBAL to permit dragging data between apps.
    // This example provides read-only access to the data.
    int flags = View.DRAG_FLAG_GLOBAL | View.DRAG_FLAG_GLOBAL_URI_READ;
    return view.startDragAndDrop(clipData,
                                 new View.DragShadowBuilder(view),
                                 null,
                                 flags);
}).attach();

타겟 드래그 앤 드롭 활동

Kotlin

// Container where the image is to be dropped in the target app.
val targetImageView = findViewById<ImageView>(R.id.imageView)

targetImageView.setOnDragListener { view, event ->

    when (event.action) {

        ACTION_DROP -> {
            val imageItem: ClipData.Item = event.clipData.getItemAt(0)
            val uri = imageItem.uri

            // Request permission to access the image data being dragged into
            // the target activity's ImageView element.
            val dropPermissions = requestDragAndDropPermissions(event)
            (view as ImageView).setImageURI(uri)

            // Release the permission immediately afterward because it's no
            // longer needed.
            dropPermissions.release()
            return@setOnDragListener true
        }

        // Implement logic for other DragEvent cases here.

        // An unknown action type is received.
        else -> {
            Log.e("DragDrop Example", "Unknown action type received by View.OnDragListener.")
            return@setOnDragListener false
        }

    }
}

Java

// Container where the image is to be dropped in the target app.
ImageView targetImageView = findViewById(R.id.imageView);

targetImageView.setOnDragListener( (view, event) -> {

    switch (event.getAction()) {

        case ACTION_DROP:
            ClipData.Item imageItem = event.getClipData().getItemAt(0);
            Uri uri = imageItem.getUri();

            // Request permission to access the image data being dragged into
            // the target activity's ImageView element.
            DragAndDropPermissions dropPermissions =
                requestDragAndDropPermissions(event);

            ((ImageView)view).setImageURI(uri);

            // Release the permission immediately afterward because it's no
            // longer needed.
            dropPermissions.release();

            return true;

        // Implement logic for other DragEvent cases here.

        // An unknown action type was received.
        default:
            Log.e("DragDrop Example","Unknown action type received by View.OnDragListener.");
            break;
    }

    return false;
});

간소화된 드래그 앤 드롭을 위한 DropHelper

DropHelper 클래스는 드래그 앤 드롭 기능의 구현을 간소화합니다. Jetpack DragAndDrop 라이브러리의 구성원인 DropHelper는 API 수준 24까지 하위 호환성을 제공합니다.

DropHelper를 사용하면 드롭 타겟을 지정하고 드롭 타겟 강조표시를 맞춤설정하며 드롭된 데이터의 처리 방법을 정의할 수 있습니다.

드롭 타겟 지정

DropHelper.configureView()는 드롭 타겟을 지정할 수 있는 오버로드된 정적 메서드입니다. 매개변수에는 다음이 포함됩니다.

예를 들어 이미지를 허용하는 드롭 타겟을 만들려면 다음 메서드 호출 중 하나를 사용하세요.

Kotlin

configureView(
    myActivity,
    targetView,
    arrayOf("image/*"),
    options,
    onReceiveContentListener)

// or

configureView(
    myActivity,
    targetView,
    arrayOf("image/*"),
    onReceiveContentListener)

Java

DropHelper.configureView(
    myActivity,
    targetView,
    new String[] {"image/*"},
    options,
    onReceiveContentlistener);

// or

DropHelper.configureView(
    myActivity,
    targetView,
    new String[] {"image/*"},
    onReceiveContentlistener);

두 번째 호출에서는 드롭 타겟 구성 옵션이 생략됩니다. 이 경우 드롭 타겟 강조표시 색상은 테마의 보조 (또는 강조) 색상으로 설정되고 강조표시 모서리 반경은 16dp로 설정되며 EditText 구성요소 목록은 비어 있습니다. 자세한 내용은 다음 섹션을 참고하세요.

드롭 타겟 구성

DropHelper.Options 내부 클래스를 사용하면 드롭 타겟을 구성할 수 있습니다. 클래스의 인스턴스를 DropHelper.configureView(Activity, View, String[], Options, OnReceiveContentListener) 메서드에 제공합니다. 자세한 내용은 이전 섹션을 참고하세요.

드롭 타겟 강조표시 맞춤설정

DropHelper는 드롭 타겟을 구성하여 사용자가 타겟 위로 콘텐츠를 드래그할 때 강조표시를 표시합니다. DropHelper는 기본 스타일을 제공하며 DropHelper.Options를 사용하면 강조표시 색상을 설정하고 강조표시 직사각형의 모서리 반경을 지정할 수 있습니다.

다음 예와 같이 DropHelper.Options.Builder 클래스를 사용하여 DropHelper.Options 인스턴스를 만들고 구성 옵션을 설정합니다.

Kotlin

val options: DropHelper.Options = DropHelper.Options.Builder()
                                      .setHighlightColor(getColor(R.color.purple_300))
                                      .setHighlightCornerRadiusPx(resources.getDimensionPixelSize(R.dimen.drop_target_corner_radius))
                                      .build()

Java

DropHelper.Options options = new DropHelper.Options.Builder()
                                     .setHighlightColor(getColor(R.color.purple_300))
                                     .setHighlightCornerRadiusPx(getResources().getDimensionPixelSize(R.dimen.drop_target_corner_radius))
                                     .build();

드롭 타겟의 EditText 구성요소 처리

또한 DropHelper는 타겟에 수정 가능한 텍스트 필드가 포함되어 있으면 드롭 타겟 내의 포커스를 제어합니다.

드롭 타겟은 단일 뷰 또는 뷰 계층 구조일 수 있습니다. 드롭 타겟 뷰 계층 구조에 하나 이상의 EditText 구성요소가 포함된 경우 구성요소 목록을 DropHelper.Options.Builder.addInnerEditTexts(EditText...)에 제공하여 드롭 타겟 강조표시 및 텍스트 데이터 처리가 올바르게 작동하도록 합니다.

DropHelper는 드래그 상호작용 중에 드롭 타겟 뷰 계층 구조 내의 EditText 구성요소가 포함 뷰에서 포커스를 훔치지 못하게 합니다.

또한, 드래그 앤 드롭 ClipData에 텍스트와 URI 데이터가 포함된 경우 DropHelper는 드롭 타겟에서 EditText 구성요소 중 하나를 선택하여 텍스트 데이터를 처리합니다. 선택은 다음 우선순위에 따라 이루어집니다.

  1. ClipData가 드롭되는 EditText입니다.
  2. 텍스트 커서 (캐럿)가 포함된 EditText입니다.
  3. DropHelper.Options.Builder.addInnerEditTexts(EditText...) 호출에 제공된 첫 번째 EditText입니다.

EditText를 기본 텍스트 데이터 핸들러로 설정하려면 EditTextDropHelper.Options.Builder.addInnerEditTexts(EditText...) 호출의 첫 번째 인수로 전달합니다. 예를 들어, 드롭 타겟이 이미지를 처리하지만 편집 가능한 텍스트 필드 T1, T2, T3을 포함하는 경우 T2를 다음과 같이 기본값으로 설정합니다.

Kotlin

val options: DropHelper.Options = DropHelper.Options.Builder()
                                      .addInnerEditTexts(T2, T1, T3)
                                      .build()

Java

DropHelper.Options options = new DropHelper.Options.Builder()
                                     .addInnerEditTexts(T2, T1, T3)
                                     .build();

드롭 타겟의 데이터 처리

DropHelper.configureView() 메서드는 드래그 앤 드롭 ClipData를 처리하기 위해 만든 OnReceiveContentListener를 허용합니다. 드래그 앤 드롭 데이터는 ContentInfoCompat 객체의 리스너에 제공됩니다. 텍스트 데이터가 객체에 있습니다. 이미지와 같은 미디어는 URI로 표현됩니다.

OnReceiveContentListenerDropHelper.configureView()를 사용하여 다음 유형의 뷰를 구성할 때 드래그 앤 드롭 외의 사용자 상호작용(예: 복사 및 붙여넣기)으로 드롭 타겟에 제공된 데이터도 처리합니다.

  • 모든 뷰(사용자가 Android 12 이상을 실행하는 경우)
  • AppCompatEditText - 사용자가 Android 7.0까지의 Android 버전을 실행하는 경우

MIME 유형, 권한, 콘텐츠 유효성 검사

DropHelper에 의한 MIME 유형 확인은 드래그 앤 드롭 데이터를 제공하는 앱에서 만든 드래그 앤 드롭 ClipDescription를 기반으로 합니다. ClipDescription의 유효성을 검사하여 MIME 유형이 올바르게 설정되었는지 확인합니다.

DropHelper는 드래그 앤 드롭 ClipData에 포함된 콘텐츠 URI의 모든 액세스 권한을 요청합니다. 자세한 내용은 DragAndDropPermissions를 참고하세요. 권한을 사용하면 드래그 앤 드롭 데이터를 처리할 때 콘텐츠 URI를 확인할 수 있습니다.

DropHelper는 드롭된 데이터의 URI를 결정할 때 콘텐츠 제공자가 반환한 데이터를 검증하지 않습니다. null을 확인하고 결정된 데이터가 정확한지 확인합니다.