강력한 고급 UI : ViewGroup을 사용자 지정하여 Github에서 거의 천 개의 별이 지닌 무적의 특수 효과를 달성하고 트렌드 목록에서 1 위를 차지하세요!

머리말

내가 자랑스러워하고, 타이틀은 거의 700 개에 불과하지만, 트렌드 목록 (당시 Kotlin 카테고리에서 3 위) 1 위를 차지했다는 점을 인정하겠습니다. 하지만 나는 여전히 의욕적 인 마음을 가지고 있으며 자랑은 결백합니다!

여러분 손에 40m 큰 칼을 내려 놓고 39m를 먼저 뛰게 하시고 이번에는 어떤 일을 할 수 있을지 한 번 보시고 저를 자르시겠습니까?

사람을 속일 수없는 그림은 없다는 것을 알고 있습니다. 먼저 최종 효과가 정말 멋진 지 확인하기 위해 그림을 넣어보세요.

아직도 쿨한 기분이 어때? ?

이 효과는 사용자 지정 ViewGroup입니다. 많은 친구가 일반적으로 사용자 지정 뷰를 더 많이 작성한다고 생각하지만 사용자 지정 ViewGroup은 더 적습니다. 주로 ViewGroup을 사용자 정의 할 때 고려해야 할 사항이 너무 많고, 측정 값이 무엇인지, 레이아웃이 무엇인지, 때로는 슬라이딩 충돌을 고려해야하는데, 이는 정말 번거롭고 성가신 일입니다.

그러나이 기사에서는 효과를 시작하고, 효과를 얻은 다음 이러한 문제를 처리하는 데 필요한 효과에 대해 약간의 분석을 제공하므로 ViewGroup을 더 얕은 것에서 더 깊은 것까지 쉽게 사용자 정의 할 수 있습니다. 엄마는 내가 더 이상 ViewGroup을 사용자 정의하지 않을 것이라고 걱정할 필요가 없습니다.

작성자 : Great God Mlx
링크 : https : //juejin.im/post/6873653741627637774
고급 UI Advanced B Station 비디오 설명 : https://www.bilibili.com/video/BV1fE411P7PA

많이 말하지 말고 여기로 오세요.

이것은이 특수 효과의 Github 주소입니다 : github.com/MlxChange/W ...

특수 효과 분석

사용자 정의 ViewGroup이기 때문에 효과가 매우 복잡합니다. 원본 렌더링을 처음 보았을 때 마음 속으로 거부했습니다. UI 디자인이 너무 멋지다고는 말할 수 없습니다. 저는 프로그래머의 노력을 전혀 고려하지 않고 단호하게 이런 종류의 작업을하지 않습니다. 것의!

그래서 UI와 싸웠는데 누가이기는지는 누구에게 달려 있습니다. 이제 우리는 효과가 달성 된 것을 보았고, 저는 정말로 향이 있다고 말하고 싶습니다 .

좋아, 더 이상 피부가 안 벗겨 졌어. 효과를 자세히 살펴 보겠습니다.

우선이 효과는 목록처럼 여러 페이지에서 볼 수 있습니다. 효과의 종류를 탐색하기 위해 이전에 효과를 본 것을 기억하세요. 왼쪽으로 슬라이드하고 오른쪽으로 슬라이드하는 것은 이렇지 않습니다. 그래서 처음에는 사용자 정의 LayoutManager또는 사용자 정의 View 에서 상속 할 수 있는지 확인하려고 생각 RecyclerView했지만 다음 인터페이스로 미리 보는 효과를 얻기가 쉽지 않아 포기하고 마지막으로 ViewGroup을 사용자 정의하기로 결정했습니다. .

이 효과의 가장 특별한 효과는 커튼을 당기는 것처럼 드래그하여 다음 인터페이스의 내용을 볼 수 있고, 중앙을 가로 질러 미끄러지면 자동으로 리바운드 효과로 반대쪽으로 미끄러질 수 있다는 것입니다. 다음 페이지를 표시합니다. 다음 페이지가 완전히 표시되면 이때 드래그 버튼이 자동으로 생성되고 다음 페이지를 다시 미리 볼 수 있습니다. 버튼을 뒤로 누를 수도 있으며 뒤로 누르면 반대 방향으로 새 버튼이 생성되어 이전 페이지의 내용을 표시합니다.

요약하면 몇 가지 요점을 요약했습니다.

  1. 관습입니다ViewGroup
  2. 하위보기는 목록처럼 스택으로 표시되며 하위보기의 콘텐츠를 사용자 지정할 수 있으며 다음 또는 이전 페이지의 콘텐츠를 미리 볼 수 있습니다.
  3. 드래그 버튼이 있는데, 잡아 당기면 버튼이 계속 커지고, 들어가면 계속 작아 져서 매우 부드러운 효과가 있습니다.
  4. 완전히 반대쪽으로 밀면 몇 번 튀어 나옵니다

사실 다른 것들은 구현하기 더 쉽다고 생각합니다. 더 번거로운 것은이 드래그 버튼입니다. 드래그 버튼부터 시작하기로 결정했습니다.

드래그 버튼

사용자 지정 ViewGroup에 대해 이야기하고 있지만 사용자 지정보기를 만드는 즉시 수행 할 수 없습니다. 먼저 사용자 지정보기를 작성하여이 드래그 버튼을 구현할 수 있는지 확인합니다. 이것이 가능하지 않으면 다음 UI에 냄비를 던질 변명을 찾으면 작성할 필요가 없습니다.

먼저이 드래그 버튼이 어떻게 구현되는지 살펴 보겠습니다.

왼쪽에있는 버튼으로 시작하면 결국 화면의 원점은 왼쪽 상단에 있습니다.

첫째, 왼쪽에서 약간의 거리에 위에서 아래로 선이 있지만 가운데에 둥글고 뾰족하지 않은 돌출부가 있습니다. 이 돌출은 신경 쓰지 않고 먼저이 선을 그립니다. 테스트이기 때문에 TestView를 만듭니다.

class TestView  @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {}

그런 다음 선을 그립니다. 이 선을 그리는 방법에 대해 생각하는 것이 좋습니다. 실제로 렌더링의 선이지만 왼쪽에도 내용이 있기 때문에 가느 다란 직사각형에 가깝다고 생각 합니다만,이 가느 다란 직사각형에는 반역자 (돌출)가 있습니다. 그래서 선을 그리는 것은 직사각형이되고,이 반역자가 존재하기 때문에 간단한 캔버스 그리기 직사각형은 분명히이 반역자를 후속 직사각형에 추가 할 수 없으므로 다른 방법으로 직사각형을 그려야합니다. 그래서 그것은 무엇입니까?

맞습니다. 즉 Path, Path동일한 효과를 얻을 수 있습니다. 그러니 계속하세요.

직사각형 그리기

브러시 및 경로 정의

var path= Path()
var paint =Paint()

init {
    paint.color=Color.RED //为了方便辨识,我们定义红色
    paint.style=Paint.Style.STROKE
    paint.isAntiAlias=true
}

직사각형을 그리는 방법? 사실 종이에 그리는 것과 똑같습니다. 원점에서 오른쪽으로 선을 그린 다음 아래쪽으로 선을 그린 다음 왼쪽으로 선을 그리고 마지막으로 원점에 가깝습니다. 그런 직사각형이 나왔습니다. 영혼 화가는 나입니다.

실제로이 그리기 방법은 Path를 달성하는 데 매우 유용하며 편의를 위해 여전히 중심점 centerX 및 centerY를 정의합니다.

override fun onDraw(canvas: Canvas) {
    super.onDraw(canvas)
    path.lineTo(100f,0f)//100只是个测试距离,画上图步骤1
    path.lineTo(100f,centerY*2)//画步骤2
    path.lineTo(0f,centerY*2)//画步骤3
    path.close()//闭合,也就是画步骤4
    canvas.drawPath(path,paint)
}

override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
    super.onSizeChanged(w, h, oldw, oldh)
    centerX= (w/2).toFloat()//定义屏幕中心X值
    centerY= (h/2).toFloat()//定义屏幕中心Y值
}

효과를 살펴 보겠습니다

emmm은 흥미로워 보이지만 대체 상태 표시 줄은 무엇입니까? 아, 걱정하지 마세요. 단지 투명한 상태 표시 줄일뿐입니다.

범프 그리기

그러면 이제이 돌출부를 그리는 방법을 고려할까요?

사실 종이에 그림을 그리면

종이에 그림을 그리는 경우에도 여전히 매우 간단합니다. 오른쪽 선에 여러 개의 돌출부가 필요합니다. 세 단계가 더 있습니다. 코드가 어떻게 구현되는지 살펴 보겠습니다.

override fun onDraw(canvas: Canvas) {
    super.onDraw(canvas)
    path.lineTo(100f,0f)//上图第一步
    path.lineTo(100f,centerY-200)//上图第二步
    path.lineTo(180f,centerY)//第三步
    path.lineTo(100f,centerY+200)//第四步
    path.lineTo(100f,centerY*2)//第五步
    path.lineTo(0f,centerY*2)//第六步
    path.close()//第七步
    canvas.drawPath(path,paint)
}

얼마나 효과적입니까?

영상

좀 재미있어 보여요 ~하지만 다른 사람의 효과는 아주 부드러워 보이고, 당신은 날카 롭고, 조금 다릅니다.

이 Xiongtai, 당신이 말한 것은 말이되지만 둥근 것을 그릴 수는 없습니다. 무엇을해야합니까? 예리한 팁만이 이런 삶을 영위 할 수 있습니다. 그 안에있는 모든 노인들은 재능 있고 착합니다. 아, 잠깐만, 옳지 않고 잘못된 세트로 가세요.

서클이란 무엇입니까? 곡선이지? 컴퓨터에서 곡선을 어떻게 그리나요? 아는 사람이 있는지 보자 10 분간 기다릴게

아, 10 분이 지났어 아무도 몰라 대답 할게

맞습니다. 베 지어 곡선입니다!

베 지어 곡선에 대해 들어 본 적이 없습니까? 형님, 나왔습니다.

베 지어 곡선은 컴퓨터에서 곡선을 시뮬레이션하는 알고리즘입니다. 방정식 balabalabla를 통해 많은 정의가 생략되었습니다.

요컨대, Bezier 곡선은 곡선을 그리는 데 도움이되며 곡선은 위치 점과 제어점에 의해 결정됩니다. 방법을 모른다면 베 지어 곡선의 기본 사용법을 배우는 것이 좋습니다.

2 차 베 지어 곡선 방법을 살펴 보겠습니다.

/**
 * 从上一个点开始,绘制二阶Bezier曲线
 * (x1,y1)为控制点, (x2,y2)为终点
 */
public void quadTo(float x1, float y1, float x2, float y2) ;

그러면 다시 베 지어 곡선으로 어떻게 그려야합니까? 종이에 그려 보자

베 지어 곡선은 3 단계에 해당합니다. 시작점은 점 A, 제어점은 점 B, 끝점은 점 C입니다.

이 지점의 좌표는 어떻습니까? 1 단계의 길이는 100이고, 점 a에서 점 B까지의 X 방향 오프셋은 100이며, 점 B의 Y 좌표가 중심 좌표이면 점 B의 좌표는 (100 + 100, centerY). 그런 다음 A에서 C까지의 거리를 200으로 설정했는데 이해하기 어렵지 않아야합니다.

코드를 연습 해 봅시다

override fun onDraw(canvas: Canvas) {
    super.onDraw(canvas)
    path.lineTo(100f,0f)//步骤1
    path.lineTo(100f,centerY-100)步骤2
    path.quadTo(200f,centerY,100f,centerY+100)//步骤3
    path.lineTo(100f,centerY*2)//步骤4
    path.lineTo(0f,centerY*2)//步骤5
    path.close()//步骤6
    canvas.drawPath(path,paint)
}

효과는 무엇입니까? 보자

Emmm은 더 이상 뜨거운 눈을 가지지 않는 것 같지만이 돌출은 실제로 약간 갑작 스럽습니다. 어떻게 보시면 승무원들에게 돈을 넣은 것 같은 느낌입니다.

당신이 이렇게 말하면 렌더링과 비교 해봤는데 이렇게했다고 느끼고 사장님은 내가 당장 해고 될까 봐 두려워합니다. 하지만 어떻게 그렇게 세련된 특수 효과를 만들었습니까?

사실 매우 간단합니다 .2 차 베 지어 곡선은 좋지 않기 때문에 3 차 베 지어 곡선이되고 3 차 베 지어 곡선은 좋지 않고 4 차와 5 차만 있습니다. . .

사실, 최종 결과는 아래와 같이 6 차 베 지어 곡선입니다.

포인트 P1과 P8은 위치 포인트이고 나머지는 제어 포인트입니다. 빨간색 선은 생성 된 곡선입니다. 매끄 럽습니까? 계획이 매우 간단합니까?

아, 큰 검을 쓰러 뜨리지 마세요! 나는 잘못을 인정했다! 사실 간단하지 않습니다. 생각했을 때 생각하기까지 오래 전부터 생각했습니다.

Tier 6은 만들기가 어렵지만 분해 할 수 있습니다. 2 개의 3 차 베 지어 곡선으로 분해해도 괜찮지 않나요?

P1을 시작점으로, P4를 끝점으로, P2와 P3을 위치 점으로 사용합니다. 반반을 그리는 것이 쉽지 않습니까?

윗부분은 다음과 같이됩니다.

3 차 베 지어 곡선 방법을 살펴 보겠습니다.

/*
 *  x1,y1是第一个控制点的位置,对应P2
 *  x2,y2是第二个控制点的位置, 对应P3
 *  x3,y3是最后一个位置点的位置,对应p4
 */
public void cubicTo(float x1, float y1, float x2, float y2,float x3, float y3)

그렇다면 그들의 좌표는 어떻게 결정됩니까?

계속 종이에 그림을 그려 보자

P1, P4는 위치 점, P2, P3은 제어점입니다. 그렇다면 P2, P3, P4의 거리를 결정하는 방법은 무엇입니까?

온라인으로 베 지어 곡선을 생성하는 웹 사이트가 많이 있는데 4 개의 점을 생성하면 진자는 거의 동일합니다. 좌표를 보면 대략적인 숫자를 알 수 있습니다.

전에 해본 적이 있는데 P1이 (0,0)이면 여러 점의 좌표는 다음과 같습니다.

p2 (0,100), p3 (90,75), p4 (100,150). P4의 Y 좌표를 centerY로 결정 했으므로 P1은 (100, centerY-150) 만 가능합니다. 코드는 다음과 같습니다.

override fun onDraw(canvas: Canvas) {
    super.onDraw(canvas)
    path.lineTo(100f,0f)//第一步
    path.lineTo(100f,centerY-150)//第二步
    path.cubicTo(100f,centerY-150f+100,100f+90f,centerY-150+75f,100f+100f,centerY)//第三步
    path.lineTo(100f,centerY*2)//第五步
    path.lineTo(0f,centerY*2)//第六步
    path.close()//第七步
    canvas.drawPath(path,paint)
}

윗부분 만 사용했기 때문에 위 그림의 네 번째 단계는 그려지지 않았고, 주로 윗부분이 원하는 효과인지 확인하고 실행하면 효과가 올 것입니다.

윗부분의 효과가 훨씬 부드러워 보이기 때문에 표주박을 그려서 아랫 부분을 마무리하겠습니다. 후반에는 P4부터 시작하겠습니다.

p1은 윗부분에있는 p4의 위치이며 윗부분의 상대적인 위치에 비해 p2, p3, p4의 좌표는이 때 쉽게 결정될 수 있습니다.

p1 (100f + 100f, centerY) , p2 (100f + 90f, centerY + 90f) , p3 (100f, centerY + 75)

다음은 코드입니다.

override fun onDraw(canvas: Canvas) {
    super.onDraw(canvas)
    path.lineTo(100f,0f)//第一步
    path.lineTo(100f,centerY-150)//第二步
    path.cubicTo(100f,centerY-150f+100,100f+90f,centerY-150+75f,100f+100f,centerY)//第三步
    path.cubicTo(100f+90f,centerY+90f,100f,centerY+75f,100f,centerY+150f)//第四步
    path.lineTo(100f,centerY*2)//第五步
    path.lineTo(0f,centerY*2)//第六步
    path.close()//第七步
    canvas.drawPath(path,paint)
}

아무 말도 할 이유 없어 효과가 마지막 인 걸 보자

효과가 매우 부드럽고 매끄럽고 더 이상 선명하지 않습니다. Mlx, 예!

이 효과가 완성되었으므로 다음 단계 인 애니메이션을 시작합니다!

드래그 애니메이션

우선 위의 애니메이션을 분석 해보면이 작은 돌기가 손가락을 따라 위아래로 움직인다는 것을 알 수 있으며, 손가락이 오른쪽으로 밀면 돌기가 커지고 먼저 위로 미끄러지는 상황을 고려합니다. 왼쪽과 오른쪽 상황에 관계없이 아래로.

상하 애니메이션

위에 그려진 돌출에서 돌출의 가장 높은 지점에 대해 설정 한 Y 값은 centerY입니다. 돌출이 이동하는 경우 이러한 점의 상대적 위치는 변경되지 않아야합니다. 유일한 변화는 돌출의 가장 높은 지점입니다.

그렇다면 문제는 돌출부의 가장 높은 지점이 어떻게 변경되어야 하는가입니다.

맞습니다. 손가락을 위아래로 따라 가세요. 지금 상황입니다. 돌출부의 가장 높은 지점은 위쪽과 아래쪽 절반의 위치입니다. Y 값은 중앙 Y입니다. 이제 Y 값은 손가락을 따라 가고 다른 상대 거리는 변경되지 않습니다.

이 사진을 예로 들면 P4의 Y 값은 변하고 P1, P2, P3의 X 좌표는 변하지 않고 Y 좌표는 P4의 좌표를 기준으로합니다. 이러한 방식으로 돌출부는 위아래로만 변경 될 수 있습니다.

다시 말해,

P1 (100, P4.y-150), P2 (100, P4.y-50), P3 (190, P4.y-75) P4 (200, Y)

여기서 유일한 변수는 Y입니다. Y는 손가락을 따르므로 손가락을 눌렀을 때 Y 값입니다. 기록하는 방법?

헤이, 그게 onTouchEvent방법 이야

먼저 현재 손가락 위치를 기록하기 위해 변수 currentY를 정의합니다. 터치가없는 경우 기본값은 화면 중앙입니다.

var currentY=0f//记录当前手指触摸位置
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
        super.onSizeChanged(w, h, oldw, oldh)
        centerX= (w/2).toFloat()
        centerY= (h/2).toFloat()
        currentY=centerY //默认为屏幕中心
}

이제 터치 위치를 기록해야합니다.

override fun onTouchEvent(event: MotionEvent): Boolean {
    when(event.action){
        MotionEvent.ACTION_MOVE-> {
            currentY=event.y
            invalidate()//重新绘制界面
        }
    }
    return true
}

우리가 그리는 곳은 아직 변경되지 않았고 P4 표준도 아직 채택되지 않았습니다. 지금 변경합시다. centerY였으며 이제 이전 버전을 대체했습니다 currentY.

override fun onDraw(canvas: Canvas) {
    super.onDraw(canvas)
    path.lineTo(100f,0f)
    path.lineTo(100f,currentY-150)
    //画上半部分
    path.cubicTo(100f,currentY-150f+100,100f+90f,currentY-150+75f,100f+100f,currentY)
    //画下半部分
    path.cubicTo(100f+90f,currentY+90f,100f,currentY+75f,100f,currentY+150f)
    path.lineTo(100f,centerY*2)
    path.lineTo(0f,centerY*2)
    path.close()
    canvas.drawPath(path,paint)
}

아하, 효과를보고 기뻐요 ~

???

형님, 정말 농담이 아닙니다. 우리는 작은 실수를 저질렀습니다. 경로가 경로라고 생각 하시죠? 경로를 추가했지만 이전 경로는 여전히 존재하므로 이러한 경로가 중첩됩니다. 따라서 경로를 그린 후 매번 원래 경로를 삭제해야합니다.

이렇게 :

override fun onDraw(canvas: Canvas) {
    ...
    canvas.drawPath(path,paint)//画路径
    path.reset()//画完之后删除之前的路径
}

그럼 다시 한번 보시죠?

괜찮아요? 그것은 완벽! 거짓말 안했어 ~

왼쪽 및 오른쪽 애니메이션

위아래 문제가 처리 된 후 좌우 슬라이딩 이벤트의 차례입니까?

효과를 다시 분석 해보자 ~

렌더링을 다시 관찰하면

바깥쪽으로 드래그하는 과정에서이 작은 돌출부가 커지지 만 돌출부가 화면 중앙에 도달하면 가장 커지고 더 이상 커지지 않습니다.

내가 커지면 무슨 뜻이야? 나는 당신이 운전하고 있다고 생각합니다. . .

돌기가 커지고 그림을 사용하면 알기 쉽게 종이에 계속 그립니다

처음에는 돌기가 이렇게되는데, 커지면 이렇게됩니다

무엇이 변경되었는지 살펴 보겠습니다.

P1과 P4 사이의 거리가 변한 것을 명확하게 알 수 있는데, Y 방향에서 P1과 P4 사이의 거리를 돌출 반경이라고합시다. P1에서 P4까지의 거리가 Y 방향으로 전체 돌출부의 절반이기 때문에 ~

그러면 반지름이 커지고 P1에서 P4까지 X 방향으로도 커졌다는 것을 알 수 있습니다.

다른 그림을 사용하여

파란색 직사각형이있는 것을 알 수 있습니다. 간단히 말해서 돌출부가 커져서 파란색 직사각형이 계속 커지는 것을 의미합니다.이 직사각형의 길이는 전체 돌출부의 길이이고이 직사각형의 너비는 돌출의 폭. 물론 제가 여기서 그린 것은 그리 비슷하지 않습니다. 결국 영혼은 손을 그립니다.

따라서 P1, P2, P3 및 P4의 좌표는이 직사각형으로 나타낼 수 있습니다. 이 직사각형의 길이 dragHeight와 너비를 정의 할 수 있으며 길이 dragWidth는 Y 방향에 해당하고 너비는 X 방향에 해당합니다.

다음과 같습니다.

위 그림에서 직사각형의 너비는로 정의되고 dragWidth직사각형의 길이는로 정의됩니다 dragHeight. 많은 테스트를 거쳐 최상의 결과를 얻은 후 좌표 관계는 다음과 같습니다. 그리고 우리는 이미 P4의 Y 좌표가 손가락 터치의 현재 Y라는 것을 알고 있습니다. 그러면 최종 좌표는 다음과 같습니다.

P1 (x, currentY-dragHeight), P2 (x, (P1.Y + P4.Y) / 2 + 12dp)
P3 ((x + dragWidth) * 0.94, (P1.Y + P4.Y) / 2), P4 (x + dragWidth, currentY)

과,

dragHeight = dragWidth ∗ 1.5

봤어? 그것은 당신에게 dragHeight달려 dragWidth있습니다. 모든 dragHeight결정 의 네 지점의 좌표가 온다.

그렇다면 dragWidth어떻게 결정됩니까? 똑똑한 친구들은 네,이 dragWidth는 손가락이 좌우로 드래그하는 거리에 의해 결정된다는 것을 알았을 것입니다. 그러나 좋은 사용자 경험을 위해 손가락으로 드래그 버튼을 막지 않도록하세요. 돌출부의 가장 높은 지점은 손가락을 누른 위치의 왼쪽에 약간 있어야합니다. currentX손가락 터치 포인트의 X 값으로 정의 되며 다음 공식이 있습니다.

dragWidth = currentX−12dp

어떤 친구들은 당신이 무엇에 대해 다했는지 물어 보았을 지 모릅니다. 많은 공식이 나오고 그것은 플레이하는 것을 불쾌하게 만듭니다.

사실 저도 같은 방법을 가지고 있습니다.이 매개 변수는 실제로 제가 조정 한 것입니다. 직접 사용할 수 있습니다. 기분이 좋지 않으면 다음 매개 변수를 수동으로 수정할 수도 있습니다.

이제 이러한 매개 변수가 있으므로 이전 코드를 수정하고 먼저이 직사각형의 길이와 너비를 정의합니다.

var dragWidth=0
var dragHeight=0

그런 다음 이전 제어점과 데이터 포인트의 좌표를 수정하는데 데이터를 직접 채우는 것이 번거 롭습니까? 어느 포인트가 어느 포인트인지 모르겠습니다. 따라서 그래프에 따라 아래 그림의 1 ~ 8 지점에 해당하는 7 개의 지점을 정의합니다.

평소처럼 ondraw에서는 정의 할 수 없습니다 ~ 그림을 이해하기 쉽도록 그림의 포인트에 따라 직접 이름을 지정했습니다. 여러분, 제가 빨간색으로 표시 한 포인트를보세요. 진짜 요점. 그리고 여러분, 내 이름에 대해 말하지 마십시오. 곡선은 2 개의 3 차 베 지어 곡선으로 구성되어 있습니다.

//上半部分
private val point1=PointF(0f,0f)
private val point2=PointF(0f,0f)
private val point3=PointF(0f,0f)
//最右边
private val point4=PointF(0f,0f)
//下半部分
private val point5=PointF(0f,0f)
private val point6=PointF(0f,0f)
private val point7=PointF(0f,0f)

따라서 그리기 코드를 수정합니다.

override fun onDraw(canvas: Canvas) {
    super.onDraw(canvas)
    //下面分别是设置对应的七个点的坐标
    point1.x=100f
    point1.y=currentY-dragHeight

    point2.x=100f
    point2.y=(currentY+point1.y)/2 + 30

    point3.x=(100f+dragWidth)*0.94f
    point3.y=(point1.y+currentY)/2

    point4.x=100f+dragWidth
    point4.y=currentY

    point7.x=100f
    point7.y=currentY+dragHeight

    point5.x=(100f+dragWidth)*0.94f
    point5.y=(currentY+point7.y)/2

    point6.x=100f
    point6.y=currentY+dragHeight/2 - 30

    //第一步
    path.lineTo(100f,0f)
    //第二步
    path.lineTo(point1.x,point1.y)
    //第三步,画上半部分
    path.cubicTo(point2.x,point2.y,point3.x,point3.y,point4.x,point4.y)
    //第四步,画下半部分
    path.cubicTo(point5.x,point5.y,point6.x,point6.y,point7.x,point7.y)
    //第五步
    path.lineTo(100f,centerY*2)
    //第六步
    path.lineTo(0f,centerY*2)
    //第七步
    path.close()
    canvas.drawPath(path,paint)
    path.reset()
}

좋습니다, 완벽합니다. 효과를 확인하기 위해 실행 해 보겠습니다.

효과가 없음을 알 수 있습니다. . 하하하, 난 너에게 속았어, 너 바보 야?

그냥 농담을해서 분위기에 활기를 불어 넣으세요. 결국 모두들 너무 오래 지켜봐 왔어요. 아마 다들 피곤할지도 몰라요. 그렇지 않으면 제가 먹을 것을 드릴까요?

사실 효과는 매우 간단합니다. 모든 공식은 직사각형의 길이와 너비를 기준으로합니다. 직사각형은 공식에서 직사각형 인 반 튀어 나온 직사각형을 나타냅니다.

그리고 직사각형의 길이와 너비는 0이므로 효과가 없습니다. 따라서 직사각형의 길이와 너비에 대한 초기 값을 설정해야합니다. 직사각형의 길이가 너비의 1.5 배라는 것을 알고 있으므로 너비에 대한 초기 값만 설정하면됩니다. 여기서 초기 값은 일시적으로 100으로 설정됩니다.

var dragWidth=100f
var dragHeight=0f
override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        dragHeight = dragWidth*1.5f
        ...
}

그래도 효과는 이전과 같지 않습니까? 용도는 무엇입니까

dei 형제를 오해하셨습니다. 이제 돌출부의 크기는 전적으로 직사각형의 길이와 너비에 의해 결정되고 직사각형의 길이도 직사각형의 너비에 의해 결정됩니다. 즉, 직사각형의 너비가이 돌출부의 크기를 결정합니다. 그런 다음 오른쪽 슬라이딩 돌출부가 커지면 본질적으로 직사각형의 너비가 커집니다. 위에서도 언급했습니다.

그러니 터치 이벤트에서 직사각형의 너비 만 변경하면 돌출이 변경됩니다. 믿지 않으시면

override fun onTouchEvent(event: MotionEvent): Boolean {
    when(event.action){
        MotionEvent.ACTION_MOVE-> {
            currentY=event.y
            //更新矩形宽度
            dragWidth=event.x-30
            invalidate()
        }
    }
    return true
}

효과가 어떤지 확인하십시오.

이봐, 흥미 롭군. 효과는 기본적으로 동일하지만 여전히 두 점입니다.

  • 화면 중앙 아래로만 슬라이드 할 수 있으며 더 이상 스 와이프 할 수 없습니다.
  • 화면 중앙으로 밀리지 않으면 자동으로 초기 위치로 돌아갑니다.

우리는 하나씩 해결합니다

슬라이딩 거리 제한

사실 이것은 매우 간단합니다. 현재 터치 이벤트의 X 값이 centerX를 초과하면 centerX를 유지합니다.

override fun onTouchEvent(event: MotionEvent): Boolean {
    when(event.action){
        MotionEvent.ACTION_MOVE-> {
            currentY=event.y
            //判断当前触摸事件的X值是否超过了屏幕中心
            dragWidth = if(event.x>centerX){
                centerX-30
            }else{
                event.x-30
            }
            invalidate()
        }
    }
    return true
}

변경은 매우 간단합니다. 여기에는 효과가 없습니다. 화면 중앙을 가로 질러 슬라이드 할 수 없습니다.

손을 떼고 초기 위치로 돌아갑니다.

화면 중앙에 도달하지 않았을 때 놓으면 초기 위치로 돌아갑니다. 간단하지 않나요? 직사각형 너비를 초기 값 100으로 설정하는 것으로 충분하지 않을까요?

맞습니다. 먼저 판단하고 놓아주세요. 어디로 놔둘 수 있나요 터치 이벤트 방식에 있어야합니다.

override fun onTouchEvent(event: MotionEvent): Boolean {
    when(event.action){
        ...
        //手指离开屏幕
        MotionEvent.ACTION_UP->{
                //如果矩形宽度小于屏幕中心
                if(dragWidth < centerX){
                    dragWidth=100f //回到初始位置
                    invalidate() //更新界面
               }
        }
    }
    return true
}

간단하지 않나요? 손이 온다 ~

Emmm, 원래 위치로 돌아 왔는데 상대방이 조금 뒤로 돌아 갔어요. 약간의주의없이 ~~~

다시 돌아가려면 어떻게해야합니까?

아, 우리 오랜 친구 속성 애니메이션이 다시 나타났습니다. 예, 오랜 친구의 속성 애니메이션에 의존해야만 달성 할 수 있습니다. 속성 애니메이션은 모든 사람에게 친숙합니다. 먼저 속성 애니메이션을 사용자 정의한 다음 시간에 따라 천천히 초기 위치로 돌아갑니다. 그리고 여기서 우리는 원래 효과가 실제로 보간자인 레트로 효과를 가지고 있다는 사실에 주목해야합니다 OvershootInterpolator. 커스텀 추정기 및 보간 기의 길이와 효과로 인해이 두 가지 사용자 정의는 매우 간단하므로 소개하지 않겠습니다.

그런 리바운드 애니메이션을 정의하기 시작했습니다.

private val dragReboundAnimator = ValueAnimator.ofFloat(0f, 1f)
private var reboundLength = 0f //需要回弹的距离是多少
private var dragReboundX = 0f // 初始回弹地点的X值
init{
    ...
    dragReboundAnimator.doOnStart {
            reboundLength = dragWidth -100
            dragReboundX = dragWidth
   }
   dragReboundAnimator.duration = 700
   dragReboundAnimator.interpolator = OvershootInterpolator(3f)
   dragReboundAnimator.addUpdateListener {
      dragWidth = dragReboundX - it.animatedValue as Float * reboundLength
      invalidate()
  }
}

먼저 이러한 변수의 의미를 설명하겠습니다.

  • dragReboundAnimator0 0.1 0.2에서 1과 같이 0에서 1까지의 속성 애니메이션을 정의합니다.
  • reboundLength예를 들어, 제 dragWidth는 직사각형 너비가 400이고 초기 위치는 100이므로 반동 거리는 300입니다. 즉, 300의 거리는 약간의 반발이 필요함을 의미합니다. 너비가 600이면 500의 거리가 조금 뒤로 돌아갑니다.
  • dragReboundX처음 놓았을 때 어디서 리바운드를 시작했는지 알아야합니다.

전체 과정은 이렇습니다. 리바운드가 필요할 때 먼저 리바운드의 초기 위치와 리바운드가 필요한 거리를 기록한 다음 리바운드의 초기 위치에서 리바운드 * 애니메이션 값의 거리를 뺀 값을 업데이트합니다. 그게 무슨 뜻입니까? 즉, 초기 거리는 600, 반동 거리는 600-100 = 500, 애니메이션 값의 초기 값은 0입니다. 즉, 다음과 같습니다.

600-500 * 0 = 600
, 600 500 0.1 = 550
600 -500
1 = 100.

이것은 약간의 리바운드를 달성합니다. 그리고 우리는 OvershootInterpolator(3f)보간기를 적용하여 머리 위로 향했다가 다시 올바른 위치로 돌아갈 수 있도록했습니다.

효과를 살펴 보겠습니다

확인! 효과는 이미 아주 좋습니다! 원본 그림과 거의 같은 느낌입니다. 이 글은 당분간 최적화되지 않을 것이며 다음 글에서 이야기 할 것입니다.

요약

Yelling은 7,000 개 이상의 단어를 썼지 만 사용자 정의 할 ViewGroup것이 너무 많고 이야기 할 것도 많습니다. 그래서 가능한 한 간결하게하려고 노력했지만 기사를 완성하는 것은 현실적이지 않습니다. 또한 친구들도 이곳을보고 피곤해서 쉬어야한다고 생각합니다.

그래서 여기서 요약하겠습니다.

View를 커스터마이징 할 경우 효과를 조금 분석해야하는데, 먼저 비슷한 것을 만든 다음 천천히 수정하고 최적화합니다. 이런 종류의 것은 특히 초보자에게 매우 중요합니다. 그래서 누구나 내 생각에 따라 천천히 경험할 수 있습니다.

따라서 최종 효과는 ViewGroup이지만이 기사에서 설명하는 대부분의 내용은 사용자 지정보기입니다. 사용자 지정 ViewGroup의 궁극적 인 목표는 여전히 사용자 지정보기의 효과이기 때문입니다. 효과를 얻을 수 없으면 모든 것이 비어 있습니다. ~

다음 섹션에서는 반대편으로 이동하는 방법과 ViewGroup에 대한 지식을 커스터마이징하는 방법을 설명합니다 ~ 원하시면 좋아요를 눌러 주시고 저를 팔로우하고 더 많은 동기를 부여해주세요.

View와 NDK를 커스터마이즈하고 소스 코드를 연구하는 것을 좋아하는 약간의 야옹을 팔로우 해 주셔서 감사합니다 ~

더 많은 Android 튜토리얼 시리즈, Android 개발 고급 고급 연구 노트, Golden nine Silver 10 인터뷰 주제 시리즈 자료가 GitHub에 업로드됩니다. https://github.com/Timdk857/Android-Architecture-knowledge-2-

추천

출처blog.csdn.net/Androiddddd/article/details/108713489