English | 简体中文 | 繁體中文 | Русский язык | Français | Español | Português | Deutsch | 日本語 | 한국어 | Italiano | بالعربية

Android 커스텀 뷰로 팔레트 효과 프로그레스 바 PendulumView 구현

인터넷에서 IOS 컴포넌트 PendulumView를 보고, 진동의 애니메이션 효과를 구현했습니다. 원생의 프로그레스바는 매우 보기 나쁘기 때문에, 이와 같은 효과를 사용자 정의 View로 구현하고, 이를 앞으로 페이지 로드 프로그레스바로 사용하고자 했습니다. 

말도 많지 않습니다. 먼저 효과 이미지를 보여드리겠습니다.

 

하단의 검은 테두리는 녹음 중 실수로 녹음된 것이며 무시할 수 있습니다. 

사용자 정의 View이므로 표준 프로세스에 따라 진행해야 합니다. 첫 번째 단계는 사용자 정의 속성입니다. 

사용자 정의 속성 

속성 파일을 만들기 

Android 프로젝트의 res->values 디렉토리에 attrs.xml 파일을 새로 만들어야 합니다. 파일 내용은 다음과 같습니다:

 <?xml version="1.0" encoding="utf-8"?>
<resources>
 <declare-styleable name="PendulumView">
  <attr name="globeNum" format="integer"/>
  <attr name="globeColor" format="color"/>
  <attr name="globeRadius" format="dimension"/>
  <attr name="swingRadius" format="dimension"/>
 </declare-styleable>
</resources>

그 중 declare-styleable의 name 속성은 코드에서 이 속성 파일을 참조하는 데 사용됩니다. name 속성은 일반적으로 우리가 정의한 View의 클래스 이름을 적습니다. 이는 매우 직관적입니다.

styleale을 사용하면 시스템이 우리의 개발 작업을 대체로 완료할 수 있는 많은 상수(int[] 배열, 인덱스 상수) 등을 작성해 주며, 우리의 개발 작업을 간소화할 수 있습니다. 예를 들어, 아래 코드에서 사용된 R.styleable.PendulumView_golbeNum과 같은 것들은 시스템이 자동으로 생성해 주는 것입니다. 

globeNum属性表示小球数量,globeColor表示小球颜色,globeRadius表示小球半径,swingRadius表示摆动半径 

读取属性值 

在自定view的构造方法中通过TypedArray读取属性值 

通过AttributeSet同样可以获取属性值,但是如果属性值是引用类型,则得到的只是ID,仍需继续通过解析ID获取真正的属性值,而TypedArray直接帮助我们完成了上述工作。 

public PendulumView(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    //使用TypedArray读取自定义的属性值
    TypedArray ta = context.getResources().obtainAttributes(attrs, R.styleable.PendulumView);
    int count = ta.getIndexCount();
    for (int i = 0; i < count; i++) {
      int attr = ta.getIndex(i);
      switch (attr) {
        case R.styleable.PendulumView_globeNum:
          mGlobeNum = ta.getInt(attr, 5);
          break;
        case R.styleable.PendulumView_globeRadius:
          mGlobeRadius = ta.getDimensionPixelSize(attr, (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_PX, 16, getResources().getDisplayMetrics()));
          break;
        case R.styleable.PendulumView_globeColor:
          mGlobeColor = ta.getColor(attr, Color.BLUE);
          break;
        case R.styleable.PendulumView_swingRadius:
          mSwingRadius = ta.getDimensionPixelSize(attr, (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_PX, 16, getResources().getDisplayMetrics()));
          break;
      }
    }
    ta.recycle(); //다음 번 읽을 때 문제가 발생하지 않도록 합니다
    mPaint = new Paint();
    mPaint.setColor(mGlobeColor);
  }

OnMeasure() 메서드를 재정의합니다 

@Override
  protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    int widthMode = MeasureSpec.getMode(widthMeasureSpec);
    int widthSize = MeasureSpec.getSize(widthMeasureSpec);
    int heightMode = MeasureSpec.getMode(heightMeasureSpec);
    int heightSize = MeasureSpec.getSize(heightMeasureSpec);
    //높이는 빛구슬의 반지름으로 설정됩니다+진동 반지름
    int height = mGlobeRadius + mSwingRadius;
    //너비는2*진동 반지름+(빛구슬의 개수-1)*빛구슬의 직경
    int width = mSwingRadius + mGlobeRadius * 2 * (mGlobeNum - 1) + mSwingRadius;
    //측정 모드가 EXACTLY인 경우, 권장 값을 직접 사용하며, EXACTLY가 아니면 (일반적으로 wrap_content를 처리하는 경우) 자신이 계산한 너비와 높이를 사용합니다
    setMeasuredDimension((widthMode == MeasureSpec.EXACTLY) ? widthSize : width, (heightMode == MeasureSpec.EXACTLY) ? heightSize : height);
  }

그 중
 int height = mGlobeRadius + mSwingRadius;
<pre name="code" class="java">int width = mSwingRadius + mGlobeRadius * 2 * (mGlobeNum - 1) + mSwingRadius;
AT_MOST로 설정된 측정 모드를 처리하는 데 사용됩니다. 일반적으로 custom View의 너비와 높이는 wrap_content로 설정되어 있으며, 이 경우 빛구슬의 개수, 반지름, 진동 반지름 등을 계산하여 View의 너비와 높이를 결정합니다. 아래와 같습니다: 

빛구슬의 개수로5이 예제에서, View의 크기는 아래의 빨간 사각형 영역입니다. 

onDraw() 메서드를 재정의합니다 

@Override
  protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    //왼쪽과 오른쪽 구체를 제외한 다른 구체를 그립니다
    for (int i = 0; i < mGlobeNum - 2; i++) {
      canvas.drawCircle(mSwingRadius + (i + 1) * 2 * mGlobeRadius, mSwingRadius, mGlobeRadius, mPaint);
    }
    if (mLeftPoint == null || mRightPoint == null) {
      //최초 왼쪽과 오른쪽 구체 좌표 초기화
      mLeftPoint = new Point(mSwingRadius, mSwingRadius);
      mRightPoint = new Point(mSwingRadius + mGlobeRadius * 2 * (mGlobeNum - 1), mSwingRadius);
      //비행 애니메이션 시작
      startPendulumAnimation();
    }
    //왼쪽과 오른쪽 구체를 그립니다
    canvas.drawCircle(mLeftPoint.x, mLeftPoint.y, mGlobeRadius, mPaint);
    canvas.drawCircle(mRightPoint.x, mRightPoint.y, mGlobeRadius, mPaint);
  }

onDraw() 메서드는自定义 View의 핵심입니다. 이 메서드 내에서 View의 표시 효과를 그립니다. 코드는 왼쪽과 오른쪽 구체를 제외한 다른 구체를 먼저 그린 다음, 좌표 값을 판단합니다. 첫 번째 그리기일 경우, 좌표 값이 모두 비어 있으면, 두 구체의 좌표를 초기화하고 애니메이션을 시작합니다. 마지막으로 mLeftPoint와 mRightPoint의 x, y 값을 사용하여 왼쪽과 오른쪽 구체를 그립니다. 

mLeftPoint와 mRightPoint는 android.graphics.Point 객체이며, 그들은 왼쪽과 오른쪽 작은 구체의 x, y 좌표 정보를 저장하는 데 사용됩니다. 

애니메이션 속성 사용 

public void startPendulumAnimation() {
    //애니메이션 속성 사용
    final ValueAnimator anim = ValueAnimator.ofObject(new TypeEvaluator() {
      @Override
      public Object evaluate(float fraction, Object startValue, Object endValue) {
        //파라미터 fraction은 애니메이션의 완료도를 나타내며, 이를 통해 현재의 애니메이션 값을 계산합니다
        double angle = Math.toRadians(90 * fraction);
        int x = (int) ((mSwingRadius - mGlobeRadius) * Math.sin(angle));
        int y = (int) ((mSwingRadius - mGlobeRadius) * Math.cos(angle));
        Point point = new Point(x, y);
        return point;
      }
    }, new Point(), new Point());
    anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
      @Override
      public void onAnimationUpdate(ValueAnimator animation) {
        Point point = (Point) animation.getAnimatedValue();
        //현재 fraction 값을 얻습니다
        float fraction = anim.getAnimatedFraction();
        //fraction이 먼저 줄어들고 다시 증가하는지, 즉 공이 상승할 준비가 되어 있는지�断합니다
        //공이 상승할 때마다 공을 전환합니다
        if (lastSlope && fraction > mLastFraction) {
          isNext = !isNext;
        }
        //왼쪽과 오른쪽 공의 x, y 좌표 값을 지속적으로 변경하여 애니메이션 효과를 생성
        //isNext를 사용하여 왼쪽 공이 움직이는지, 오른쪽 공이 움직이는지�断하기 위해 사용됩니다
        if (isNext) {
          //좌측 공을 흔들 때, 우측 공은 초기 위치에 놓입니다
          mRightPoint.x = mSwingRadius + mGlobeRadius * 2 * (mGlobeNum - 1);
          mRightPoint.y = mSwingRadius;
          mLeftPoint.x = mSwingRadius - point.x;
          mLeftPoint.y = mGlobeRadius + point.y;
        } else {
          //우측 공을 흔들 때, 좌측 공은 초기 위치에 놓입니다
          mLeftPoint.x = mSwingRadius;
          mRightPoint.y = mSwingRadius;
          mRightPoint.x = mSwingRadius + (mGlobeNum - 1) * mGlobeRadius * 2 + point.x;
          mRightPoint.y = mGlobeRadius + point.y;
        }
        invalidate();
        lastSlope = fraction < mLastFraction;
        mLastFraction = fraction;
      }
    });
    //무한 반복 재생으로 설정
    anim.setRepeatCount(ValueAnimator.INFINITE);
    //반복 모드를 역순 재생으로 설정
    anim.setRepeatMode(ValueAnimator.REVERSE);
    anim.setDuration(200);
    //트랜지션을 설정하여 애니메이션의 변화 속도를 제어합니다
    anim.setInterpolator(new DecelerateInterpolator());
    anim.start();
  }

 ValueAnimator.ofObject 메서드를 사용하여 Point 객체를 조작할 수 있도록 하기 위해 사용되었습니다. 또한 ofObject 메서드를 통해自定义된 TypeEvaluator 객체를 사용하여 fraction 값을 얻었습니다. 이 값은 0에서-1변화하는 소수점 숫자를 얻습니다. 따라서 이 메서드의 두 번째 및 세 번째 매개변수 startValue(new Point()), endValue(new Point())은 실제 의미가 없으며, 직접 적지 않아도 됩니다. 여기서 적는 것은 이해하기 쉽게 하기 위해입니다. 마찬가지로 ValueAnimator.ofFloat(0f, 1f) 메서드를 통해 0에서-1변화하는 소수점 숫자.

     final ValueAnimator anim = ValueAnimator.ofObject(new TypeEvaluator() {
      @Override
      public Object evaluate(float fraction, Object startValue, Object endValue) {
        //파라미터 fraction은 애니메이션의 완료도를 나타내며, 이를 통해 현재의 애니메이션 값을 계산합니다
        double angle = Math.toRadians(90 * fraction);
        int x = (int) ((mSwingRadius - mGlobeRadius) * Math.sin(angle));
        int y = (int) ((mSwingRadius - mGlobeRadius) * Math.cos(angle));
        Point point = new Point(x, y);
        return point;
      }
    }, new Point(), new Point());

fraction을 통해 공이 진동할 때의 각도 변화 값을 계산합니다, 0-90도

 

mSwingRadius-mGlobeRadius는 그림에서 초록색 선의 길이를 나타내며, 진동 경로는 (mSwingRadius-mGlobeRadius)로 둘레를 이루는 곡선, 변화하는 X 값은 (mSwingRadius-mGlobeRadius)*sin(angle),변화하는 y 값은 (mSwingRadius-mGlobeRadius)*cos(angle) 

대응하는 공의 실제 원심 좌표는 (mSwingRadius-x, mGlobeRadius+y) 

오른쪽의 공의 운동 경로는 왼쪽과 유사하지만, 방향만 다릅니다. 오른쪽의 공의 실제 원심 좌표는 (mSwingRadius + (mGlobeNum - 1) * mGlobeRadius * 2 + x, mGlobeRadius+y) 

왼쪽과 오른쪽의 공의 y좌표는 같지만, x좌표만 다릅니다. 

        float fraction = anim.getAnimatedFraction();
        //fraction이 먼저 줄어들고 다시 증가하는지, 즉 공이 상승할 준비가 되어 있는지�断합니다
        //공이 상승할 때마다 공을 전환합니다
        if (lastSlope && fraction > mLastFraction) {
          isNext = !isNext;
        }
        //이전 fraction이 계속 줄어드는지 기록합니다
        lastSlope = fraction < mLastFraction;
        //이전 fraction을 기록합니다
        mLastFraction = fraction;

 이 두 개의 코드는 언제 운동을 전환할지 계산하는 데 사용되며, 이 애니메이션은 반복 재생을 설정했으며, 반복 모드는 역순 재생으로 설정되어 있어서, 애니메이션의 한 주기는 공을 던지고 공이 떨어지는 과정입니다. 이 과정에서 fraction의 값은 0에서 변하며,1,그리고}10으로 변환됩니다. 그렇다면 애니메이션의 새로운 주기가 시작되는 순간은 무엇일까요? 공이 던지기 직전의 순간입니다. 이 순간에 운동하는 공을 전환하면 왼쪽 공이 내려가고 오른쪽 공이 던지는 애니메이션 효과를 구현하고, 오른쪽 공이 내려가고 왼쪽 공이 던지는 애니메이션 효과를 구현할 수 있습니다. 

그렇다면 이 시점을 어떻게 포착할 수 있을까요? 

공이 던지는 순간 fraction 값은 지속적으로 증가하며, 공이 내려가는 순간 fraction 값은 지속적으로 감소합니다. 공이 던지기 직전의 순간, fraction이 지속적으로 감소하는 상태에서 지속적으로 증가하는 상태로 변하는 순간이 있습니다. 코드에서는 마지막 fraction이 지속적으로 감소하는지 확인한 후, 이번 fraction이 지속적으로 증가하는지 비교하고, 두 조건이 모두 성립하면 운동하는 공을 전환합니다. 

    anim.setDuration(200);
    //트랜지션을 설정하여 애니메이션의 변화 속도를 제어합니다
    anim.setInterpolator(new DecelerateInterpolator());
    anim.start();

애니메이션의 지속 시간을 설정합니다200 밀리초, 독자는 이 값을 변경하여 공의 진폭 진동 속도를 수정할 수 있습니다.

애니메이션의 트랜지션을 설정합니다. 공을 던지는 과정은 점진적으로 감속되는 과정이며, 내려가는 과정은 점진적으로 가속되는 과정이므로 DecelerateInterpolator를 사용하여 감속 효과를 구현하고, 반대 방향으로 재생할 때는 가속 효과를 구현합니다. 

시동 애니메이션, 진폭 효과의 커스터마이징 View 진행条的 실현이 완료되었습니다! 빨리 실행하여 효과를 확인해 보세요!

이것이 이 문서의 전체 내용입니다. 여러분의 학습에 도움이 되길 바라며, 다음에도 노래教程을 많이 지지해 주시길 바랍니다.

선언: 이 문서의 내용은 인터넷에서 가져왔으며, 저작권은 원작자에게 있으며, 인터넷 사용자가 자발적으로 기여하고 업로드한 내용입니다. 이 사이트는 소유권을 가지지 않으며, 인공 편집을 하지 않았으며, 관련 법적 책임도 부담하지 않습니다. 저작권 침해가 의심되는 내용을 발견하시면, notice#w 이메일로 발송하여 주십시오.3codebox.com(이메일을 보내는 경우 #을 @으로 변경하십시오. 신고를 하고 관련 증거를 제공하시면, 사실이 확인되면 이 사이트는 즉시 의심스러운 저작권 침해 내용을 삭제합니다.

좋아하는 것