Android uses View to achieve spherical rotation and scrolling effect (Mid-Autumn Festival)

citation

The sphere in this article is realized with View package, not a 3D sphere in the actual sense

The Mid-Autumn Festival is approaching recently. Thinking that the moon is so beautiful in such a festive season, I came up with the idea of ​​putting the moon in the app to present it. At the same time, I want to use the simplest View to achieve this effect. The hard work pays off. After experiencing After 3 hours of hard work, I used View to create a 3D spherical effect (not true 3D).

renderings

Capability support:

  • Eclipse rotation effect
  • The effect of rotating and moving the surface of the moon

It’s still the old rules, let’s first upload the renderings:

Please add a picture description

Core implementation code

If you don’t want to read the implementation process below, you can directly copy this piece of code and use it here. Of course, it involves a cut image that needs to be downloaded here.

package com.ftd.myapplication

import android.animation.ValueAnimator
import android.content.Context
import android.graphics.*
import android.util.AttributeSet
import android.util.Log
import android.view.View
import androidx.core.graphics.scale


class MoonView(context: Context, attributeSet: AttributeSet) : View(context, attributeSet) {
    
    


    private var moonRadius = 0f // 圆形半径

    private val zoom = 0.8f //相较外层View宽高的缩放倍率 0~1
    private var centerOvalDistance = 0f //椭圆距离中心长度 用以做月球圆弧等内容的变化
    private var bgMoveLegth = 0f //椭圆距离中心长度 用以做月球圆弧等内容的变化

    private var moonCenterX = 0f    //固定月亮的中心X位置
    private var moonCenterY = 0f    //固定月亮的中心Y位置


    private val innerMoonPath = Path() //月球切月形状


    private val moonRectF = RectF()   //月球形状
    private val centerOvalRectF = RectF() //

    private lateinit var moonBg: Bitmap //背景切图

    var moonType=MoonType.LunarEclipse //展示View的类型

    private lateinit var bgMatrix: Matrix //切图和Viwe进行适配缩放

    private val valueAnimator = ValueAnimator() //动画

    private var isLeftStart = true //是否从做开始

    var distance=1f


    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
    
    
        Log.e("TEST", "onSizeChanged${w}::${h}")

        moonRadius = if (w <= h) {
    
    
            w * zoom / 2
        } else {
    
    
            h * zoom / 2
        }
        moonCenterX = w / 2f
        moonCenterY = h / 2f

        moonRectF.left = moonCenterX - moonRadius
        moonRectF.right = moonCenterX + moonRadius
        moonRectF.top = moonCenterY - moonRadius
        moonRectF.bottom = moonCenterY + moonRadius


        centerOvalRectF.top = moonCenterY - moonRadius
        centerOvalRectF.bottom = moonCenterY + moonRadius

        val options = BitmapFactory.Options()
        options.inScaled = true
        options.inSampleSize = 1
        moonBg = BitmapFactory.decodeResource(resources, R.mipmap.moon_bg, options)
            .copy(Bitmap.Config.ARGB_8888, true)
        //绘制背景Bitmap大小的矩阵
        bgMatrix = Matrix() //如果在构造器中初始化,需要使用reset()方法
        bgMatrix.postTranslate(-moonBg.width + moonRadius * 2, moonRectF.top)
        bgMatrix.preScale(
            1f,
            h * zoom / moonBg.height
        )
        moonBg.scale(moonBg.width, h, false)

        valueAnimator.setFloatValues(moonRadius)
        valueAnimator.repeatMode = ValueAnimator.RESTART
        valueAnimator.repeatCount = -1
        centerOvalDistance = -moonRadius

        valueAnimator.addUpdateListener {
    
    
            if (moonType==MoonType.Moon){
    
    
                roateMoon(distance)
            }else if (moonType==MoonType.LunarEclipse){
    
    
                moveMoon(distance)
            }
            postInvalidate()
        }

        valueAnimator.start()
        super.onSizeChanged(w, h, oldw, oldh)

    }


    override fun onDraw(canvas: Canvas?) {
    
    
        super.onDraw(canvas)

        if (centerOvalDistance <= 0) {
    
    
            centerOvalRectF.left = moonCenterX + centerOvalDistance
            centerOvalRectF.right = moonCenterX - centerOvalDistance
        } else {
    
    
            centerOvalRectF.left = moonCenterX - centerOvalDistance
            centerOvalRectF.right = moonCenterX + centerOvalDistance
        }//计算内部椭圆左右弧线的矩形框

        innerMoonPath.reset()
        canvas!!.save()

        innerMoonPath.reset()
        if (isLeftStart) {
    
    
            innerMoonPath.arcTo(moonRectF, 90f, 180f, true)
        } else {
    
    
            innerMoonPath.arcTo(moonRectF, 270f, 180f, true)
        }//画外圆左右弧线

        if (centerOvalDistance <= 0) {
    
    
            innerMoonPath.arcTo(centerOvalRectF, 270f, 180f)
        } else {
    
    
            innerMoonPath.arcTo(centerOvalRectF, 90f, 180f)
        }//画内部椭圆左右弧线
        innerMoonPath.fillType = Path.FillType.EVEN_ODD

        canvas.clipPath(innerMoonPath)  //裁剪
        canvas.drawBitmap(moonBg, bgMatrix, null)


    }

    //仅旋转月亮
    private fun roateMoon(distance: Float) {
    
    
        if (bgMoveLegth >= moonBg.width / 2 + moonRadius) {
    
    
            bgMoveLegth -= moonBg.width / 2
            bgMatrix.postTranslate((-moonBg.width / 2).toFloat(), 0f)
        } else {
    
    
            bgMoveLegth += distance
            bgMatrix.postTranslate(distance, 0f)
        }
        postInvalidate()
    }


    //旋转月亮加月食效果
    private fun moveMoon(distance: Float) {
    
    
        if (centerOvalDistance <= -moonRadius) {
    
    
            centerOvalDistance = moonRadius
            isLeftStart = !isLeftStart
        }
        centerOvalDistance -= distance
        roateMoon(distance)
    }


    //类型
     enum class MoonType {
    
    
        LunarEclipse,//月食
        Moon
    }
}

Realization principle

spherical rolling

The implementation principle is actually relatively simple. As follows, we place a Bitmap section below. We display this section according to the shape we want, and at the same time, we can continuously scroll (slide) this section to achieve a 3D-like effect.
insert image description here

Cut the picture and pay attention

Of course, it is best to splice this cut image with two complete spherical slices, so that the effect of continuous rolling can be achieved. The following is my spliced ​​lunar slice: the original material of the lunar slice can be downloaded
Please add a picture description
from Nasa by clicking this link, and the spliced ​​material can be downloaded here

Of course, you can achieve other planet scrolling effects only if you change this section material, the principle is the same. If you have such a useful material URL, you can leave a message below, I have been searching for a long time and haven't found any other ones, it's too difficult!
The following is the relevant code for scrolling and rotation, which is implemented with Matrix

 //仅旋转月亮
    private fun roateMoon(distance: Float) {
    
    
        if (bgMoveLegth >= moonBg.width / 2 + moonRadius) {
    
    
            bgMoveLegth -= moonBg.width / 2
            bgMatrix.postTranslate((-moonBg.width / 2).toFloat(), 0f)
        } else {
    
    
            bgMoveLegth += distance
            bgMatrix.postTranslate(distance, 0f)
        }
        postInvalidate()
    }

lunar eclipse effect

As for the lunar eclipse effect, we can achieve it by drawing two lines to cut the picture, so that the effect presented on the top layer is the shape we need. The lunar eclipse shape mainly consists of two parts, one is the
outer arc on the left side of the circle The intersection area formed by the line and the inner arc line of the inner ellipse, that is, the red area below, is the shape of the lunar eclipse we want.
related code


        if (centerOvalDistance <= 0) {
    
    
            centerOvalRectF.left = moonCenterX + centerOvalDistance
            centerOvalRectF.right = moonCenterX - centerOvalDistance
        } else {
    
    
            centerOvalRectF.left = moonCenterX - centerOvalDistance
            centerOvalRectF.right = moonCenterX + centerOvalDistance
        }//计算内部椭圆左右弧线的矩形框

        innerMoonPath.reset()
        canvas!!.save()

        innerMoonPath.reset()
        if (isLeftStart) {
    
    
            innerMoonPath.arcTo(moonRectF, 90f, 180f, true)
        } else {
    
    
            innerMoonPath.arcTo(moonRectF, 270f, 180f, true)
        }//画外圆左右弧线

        if (centerOvalDistance <= 0) {
    
    
            innerMoonPath.arcTo(centerOvalRectF, 270f, 180f)
        } else {
    
    
            innerMoonPath.arcTo(centerOvalRectF, 90f, 180f)
        }//画内部椭圆左右弧线
        innerMoonPath.fillType = Path.FillType.EVEN_ODD

        canvas.clipPath(innerMoonPath)  //裁剪
        canvas.drawBitmap(moonBg, bgMatrix, null)

insert image description here
Then by adjusting the width of the rectangle that draws the inner ellipse, so that the arc can move, you can have an animation effect of the lunar eclipse.

  //旋转月亮加月食效果
    private fun moveMoon(distance: Float) {
    
    
        if (centerOvalDistance <= -moonRadius) {
    
    
            centerOvalDistance = moonRadius
            isLeftStart = !isLeftStart
        }
        centerOvalDistance -= distance
        roateMoon(distance)
    }

At this point, a simple use of View to realize the effect of lunar eclipse is realized, and you can directly use this View in the project.

Brief introduction

The following is the relevant code. If necessary, you can directly refer to the reuse to help you quickly build the demo. Because this demo does not involve much code, it will not be passed to the warehouse.
activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <com.ftd.myapplication.MoonView
        android:layout_width="100dp"
        android:layout_height="100dp"
        app:layout_constraintBottom_toTopOf="@+id/moonView"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        android:layout_marginBottom="20dp"/>
    <com.ftd.myapplication.MoonView
        android:id="@+id/moonView"
        android:layout_width="100dp"
        android:layout_height="100dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

MainActivtiy.class

package com.ftd.myapplication

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.appcompat.widget.AppCompatButton
import com.ftd.myapplication.MoonView
import com.ftd.myapplication.R

class MainActivity : AppCompatActivity() {
    
    

    override fun onCreate(savedInstanceState: Bundle?) {
    
    
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        initView()
    }

    private fun initView() {
    
    
        val moonView = findViewById<MoonView>(R.id.moonView)
        moonView.moonType = MoonView.MoonType.Moon
        moonView.distance=2f
    }

}

at last

This implementation is relatively hasty. If you feel that it is a good idea to use View to achieve 3D effects, I can take time to optimize the effects, such as shadows and gestures, which can be considered.

At the same time, I also wish everyone a happy and happy Mid-Autumn Festival! !

Please add a picture description

Guess you like

Origin blog.csdn.net/number_cmd9/article/details/126696443