Detailed explanation of SpringAnimation

Introduction to SpringAnimation with examples

Detailed explanation and examples of SpringAnimation (Android Studio Java implementation)

translation part (this part is the translation, is the translation

illustrate

This article is translated from the link of the original text.
The original text was implemented using Kotlin. For the source code, see SpringAnimation source code Kotlin implementation
. This article has been changed to Java.
p1
r1
s1

Have you ever wanted to make an elastic animation like the one above on Android? If you have, then you will be very happy
!

Dynamic-animation is a new module introduced in version 25.3.0 of the Android Support Library. It provides several classes to animate physically based realistic views.

You might say "Anyway, I just need to add a BounceInterpolator or OvershootInterpolator to my animation". In fact, these two effects don't look very good. Of course, you could also write your own interpolator or implement a complete custom animation - but for now there's an easier way.

Classes (its class is also a text description)

At the time of writing this article, this module only contains 4 classes. Let's see their documentation description.

1. DynamicAnimation<T extends DynamicAnimation<T>>
Original text
In fact, AndroidX is now used, and this library is no longer maintained.
This class is the base class for physically based animations. It manages the animation life cycle, such as start() and cancel(). This base class also handles common settings for all subclass animations. For example, the DynamicAnimation library supports adding DynamicAnimation.OnAnimationEndListener and DynamicAnimation.OnAnimationUpdateListener so that important animation events can be listened to through callbacks. The start condition of any subclass of DynamicAnimation can be set using setStartValue(float) and setStartVelocity(float).

2. DynamicAnimation.ViewProperty
The original text
still uses AndroidX. Anyway, it seems that Android no longer maintains these libraries. And see what it is used for.
ViewProperty holds access to View properties. That is to say, when you use DynamicAnimation to create an animation, the corresponding property value of the view will be updated through this ViewProperty instance.

Let's take a look at the parameters of the classic animation:

ALPHA(透明度), ROTATION(旋转度,一般是弧度),ROTATION_X, ROTATION_Y, 
SCALE_X(缩放x的倍数), SCALE_Y(缩放y的倍数), 
SCROLL_X, SCROLL_Y, (滑动,表示其子控件与左、上之间的差值,换句话说就比如你屏幕内容过多,使用这个可以让你滑动着看,比如说现在这行字已经很多了,你可以拖动黑色背景下的滑块来查看后面的文字)
TRANSLATION_X(平移x个单位), TRANSLATION_Y, TRANSLATION_Z, 
X, Y, Z(坐标)

3.SpringAnimation

What is Spring Animation? Does spring animate? Of course not. After reading a lot of information, it is translated as spring animation. When it comes to spring, you think of Hooke's law (mechanical elasticity theory). That's right, SpringAnimation is an animation driven by SpringForce (elasticity).
The spring force defines the spring's stiffness, damping ratio, and rest position. Once a SpringAnimation is started, on each frame, the spring force will update the animation's value and speed. The animation will continue to run until the spring forces are balanced. If the spring used in the animation is undamped, the animation will never reach equilibrium. Instead, it will oscillate forever.

4. SpringForce
The spring force defines the properties of the spring used in the animation.
By configuring the stiffness and damping ratio, the caller can create a spring that looks and feels right for their use case. Stiffness corresponds to the spring constant. The stiffer the spring, the harder it is to stretch and the faster it experiences damping.
s

The spring-to-damper ratio describes how vibration dampens after a system is disturbed. When the damping ratio is > 1 (i.e. overdamped), the object will quickly return to the rest position without overshooting (overshooting: or translated as 过冲量). If the damping ratio is equal to 1 (that is, critical damping), the object will return to equilibrium in the shortest time. When the damping ratio is less than 1 (i.e. underdamped), the mass tends to overshoot, and back, overshoot again. Without any damping (i.e. damping ratio = 0), the mass will oscillate forever.
This class has 3 parameters:

finalPosition: The rest position (or angle/scale) of the spring.

stiffness:刚度
刚度对应于弹簧常数。弹簧越硬,拉伸就越困难,它经受的阻尼也就越快。刚度越高,物体沉降越快。

dampingRatio: damping ratio.
The spring-to-damper ratio describes how vibration dampens after a system is disturbed.

SpringForce has 4 predefined floating constants for stiffness and damping ratios, but it is also possible to set custom values ​​and implement them yourself.
Depending on the value, our object will:

  1. Oscillates forever around a rest position.
  2. Oscillates around its rest position until it comes to rest.
  3. Stop lightly, very quickly (not visible to the naked eye).
  4. Stops quickly, without overshoot.

As you can see, the package is currently very small. If you're looking for some more complex spring dynamics, take a look at Facebook's Rebound library , probably on the Internet for science.

Note that
DynamicAnimation does not extend Animation, so you can't just replace one or use it in an AnimationSet. But don't worry, the whole process is still very simple.

Here is a picture of wood carving in sand, hahaha, the coding process has started. The following codes are actually operated by myself. For the Java version, it depends on Kotlin to read the original text. The link address is attached at the beginning of this article.
s

Sample and detailed explanation

This part is a record of my own operation process. The source code will be placed at the end of the article. I originally wanted to earn some points (but every time I saw that the download required points, I would collapse, but the download link is attached, please don’t download it (irony)), think about it or upload it to GitHub, free and open source is the most fragrant, no Give a chance to spend points.

1. Create a new empty project

file->new project, select an empty project (empty), simple enough.
s

2. Add dependencies

Dependencies, open your buil.gradle, note that it is in the app directory, not the gradle directory. Put the project into project mode, and it looks like this:
s
Note, you need to synchronize after adding the code, probably the sync red position I wrote, click, if the build is successful, it is successful.

implementation 'androidx.dynamicanimation:dynamicanimation:1.1.0-alpha03'

What to do if adding dependencies is not successful, maybe the version is wrong. You add it like this: click file, select Project Structure
insert image description here
, enter the library name in the search box, search for it, and add it in (generally the library version that matches your android studio version is what you search for.)
s

3. Create the spring animation

//创建弹性动画类SpringAnimation
        SpringAnimation animation = new SpringAnimation(view, property);
        //SpringForce类,定义弹性特质
        SpringForce spring = new SpringForce(finalPosition);
        spring.setStiffness(stiffness);//刚度
        spring.setDampingRatio(dampingRatio);//阻尼比
        //关联弹性特质
        animation.setSpring(spring);
        animation.start();//启动动画

Move it into a function and call it when the animation is going to be created.

    @SuppressLint("Range")
    SpringAnimation createSpringAnimation(View view,
                                          DynamicAnimation.ViewProperty property,
                                          Float finalPosition,
                                          @FloatRange(from = 0.0) Float stiffness,
                                          @FloatRange(from = 0.0) Float dampingRatio) {
    
    
        //创建弹性动画类SpringAnimation
        SpringAnimation animation = new SpringAnimation(view, property);
        //SpringForce类,定义弹性特质
        SpringForce spring = new SpringForce(finalPosition);
        spring.setStiffness(stiffness);//
        spring.setDampingRatio(dampingRatio);
        //关联弹性特质
        animation.setSpring(spring);
        return animation;
    }

4.Position, the first animation demonstration.

PositionActivity.java

package com.example.mydef20;

import androidx.annotation.FloatRange;
import androidx.appcompat.app.AppCompatActivity;
import androidx.dynamicanimation.animation.DynamicAnimation;
import androidx.dynamicanimation.animation.SpringAnimation;
import androidx.dynamicanimation.animation.SpringForce;

import android.annotation.SuppressLint;
import android.os.Bundle;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewTreeObserver;

public class PositionActivity extends AppCompatActivity {
    
    
    float stiffNess= SpringForce.STIFFNESS_MEDIUM;//硬度,可设
    float damp=SpringForce.DAMPING_RATIO_HIGH_BOUNCY;//阻尼比
    SpringAnimation xAnimation;
    SpringAnimation yAnimation;//坐标
    View mView;
    float dX=0f;
    float dY=0f;
    //创建一个动画对象
    @Override
    protected void onCreate(Bundle savedInstanceState) {
    
    
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_position);
        mView=findViewById(R.id.PView);
        mView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
    
    
            @Override
            public void onGlobalLayout() {
    
    
                xAnimation=createSpringAnimation(mView,SpringAnimation.X,mView.getX(),stiffNess,damp);
                yAnimation=createSpringAnimation(mView,SpringAnimation.Y,mView.getY(),stiffNess,damp);
                mView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
            }
        });//确定初始位置,确定后移除监听器
        mView.setOnTouchListener(new View.OnTouchListener() {
    
    
            @Override
            public boolean onTouch(View v, MotionEvent event) {
    
    
                switch (event.getActionMasked()){
    
    
                    case MotionEvent.ACTION_DOWN:
                        dX=v.getX()-event.getRawX();//计算距离
                        dY=v.getY()-event.getRawY();
                        xAnimation.cancel();
                        yAnimation.cancel();//按住图片
                        break;
                    case MotionEvent.ACTION_MOVE:
                        mView.animate()
                                .x(event.getRawX()+dX)
                                .y(event.getRawY()+dY)
                                .setDuration(0)
                                .start();
                        break;
                    case MotionEvent.ACTION_UP:
                        xAnimation.start();
                        yAnimation.start();
                        break;
                    default:
                        break;
                }
                return true;
            }
        });
    }
    @SuppressLint("Range")
    SpringAnimation createSpringAnimation(View view,
                                          DynamicAnimation.ViewProperty property,
                                          Float finalPosition,
                                          @FloatRange(from = 0.0) Float stiffness,
                                          @FloatRange(from = 0.0) Float dampingRatio) {
    
    
        //创建弹性动画类SpringAnimation
        SpringAnimation animation = new SpringAnimation(view, property);
        //SpringForce类,定义弹性特质
        SpringForce spring = new SpringForce(finalPosition);
        spring.setStiffness(stiffness);//
        spring.setDampingRatio(dampingRatio);
        //关联弹性特质
        animation.setSpring(spring);
        return animation;
    }
}

activity_position.xml
src is to import image resources, img0 is a picture I placed myself, you can also put your own pictures.

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".PositionActivity">
    <ImageView
        android:id="@+id/PView"
        android:layout_width="128dp"
        android:layout_height="128dp"
        android:layout_gravity="center"
        android:src="@drawable/img0"
        tools:ignore="ContentDescription"/>
</FrameLayout>

Effect demonstration:
insert image description here
The csdn is not right. It has been uploaded for a long time but failed.
Attach this picture of the beloved cat teacher: this is img0 under drawable
img

5.Rotation, the second animation demonstration.

RotationActivity.java

package com.example.mydef20;

import androidx.annotation.FloatRange;
import androidx.appcompat.app.AppCompatActivity;
import androidx.dynamicanimation.animation.DynamicAnimation;
import androidx.dynamicanimation.animation.SpringAnimation;
import androidx.dynamicanimation.animation.SpringForce;

import android.annotation.SuppressLint;
import android.icu.lang.UProperty;
import android.os.Bundle;
import android.view.MotionEvent;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;

public class RotationActivity extends AppCompatActivity {
    
    
    float stiffNess= SpringForce.STIFFNESS_MEDIUM;//硬度,可设
    float damp=SpringForce.DAMPING_RATIO_HIGH_BOUNCY;//阻尼比
    float spine=0f;//旋转角度,负表示逆时针,正代表顺时针
    TextView rTextView;
    ImageView rView;
    SpringAnimation rotationAnimation;
    float curRotation=0f,preRotation=0f;
    float x,y;
    @SuppressLint("ClickableViewAccessibility")
    @Override
    protected void onCreate(Bundle savedInstanceState) {
    
    
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_rotation);
        rTextView= findViewById(R.id.rTextView);
        rView=findViewById(R.id.RView);
        updateRotationText();
        rotationAnimation=createSpringAnimation(rView,SpringAnimation.ROTATION,spine,stiffNess,damp);
        rotationAnimation.addUpdateListener(new DynamicAnimation.OnAnimationUpdateListener() {
    
    
            @Override
            public void onAnimationUpdate(DynamicAnimation animation, float value, float velocity) {
    
    
                updateRotationText();
            }
        });
        final float cx=rView.getWidth()/2f;
        final float cy=rView.getHeight()/2f;
        rView.setOnTouchListener(new View.OnTouchListener() {
    
    
            @Override
            public boolean onTouch(View v, MotionEvent event) {
    
    
                switch (event.getActionMasked()){
    
    
                    case MotionEvent.ACTION_DOWN:
                        x= event.getX();
                        y= event.getY();
                        //rotationAnimation.cancel();//
                        UCRotation(v,x,y,cx,cy);
                        rotationAnimation.cancel();
                        break;
                    case MotionEvent.ACTION_MOVE:
                        x=event.getX();
                        y= event.getY();//检测鼠标移动,注意,会相对当前image的位置进行旋转
                        preRotation=curRotation;
                        UCRotation(v,x,y,cx,cy);
                        float angle=curRotation-preRotation;
                        float tmp=angle+v.getRotation();
                        v.setRotation(tmp);
                        updateRotationText();
                        break;
                    case MotionEvent.ACTION_UP:
                        rotationAnimation.start();
                        break;
                    default:
                        break;
                }
                return true;
            }
        });
    }
    public void UCRotation(View v,float tx,float ty,float ex,float ey){
    
    
        curRotation=v.getRotation()+(float) (Math.toDegrees(Math.atan2(tx - ex, ey - ty)));
    }
    private void updateRotationText(){
    
    
        @SuppressLint("DefaultLocale")
        String context=String.format("%.3f", rView.getRotation());
        rTextView.setText(context);
    }
    @SuppressLint("Range")
    SpringAnimation createSpringAnimation(View view,
                                          DynamicAnimation.ViewProperty property,
                                          Float rangle,
                                          @FloatRange(from = 0.0) Float stiffness,
                                          @FloatRange(from = 0.0) Float dampingRatio) {
    
    
        //创建弹性动画类SpringAnimation
        SpringAnimation animation = new SpringAnimation(view, property);
        //SpringForce类,定义弹性特质
        SpringForce spring = new SpringForce(rangle);
        spring.setStiffness(stiffness);//
        spring.setDampingRatio(dampingRatio);
        //关联弹性特质
        animation.setSpring(spring);
        return animation;
    }
}

activity_rotation.xml

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".RotationActivity">

    <ImageView
        android:id="@+id/RView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:src="@drawable/ic_rotation"
        tools:ignore="ContentDescription"/>
    <TextView
        android:id="@+id/rTextView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:layout_marginBottom="192dp"/>
</FrameLayout>

Demo:s

6.Scale, the third animation demonstration.

ScaleActivity.java

package com.example.mydef20;

import androidx.annotation.FloatRange;
import androidx.appcompat.app.AppCompatActivity;
import androidx.dynamicanimation.animation.DynamicAnimation;
import androidx.dynamicanimation.animation.SpringAnimation;
import androidx.dynamicanimation.animation.SpringForce;

import android.annotation.SuppressLint;
import android.os.Bundle;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewTreeObserver;
import android.widget.SeekBar;

public class ScaleActivity extends AppCompatActivity {
    
    
    private SeekBar damping;
    private SeekBar stiffness;//阻尼、硬度
    View sView;
    SpringAnimation xAnimation;
    SpringAnimation yAnimation;//坐标
    float dX=0f;
    float dY=0f;
    float sX;
    float sY;
    float vxDis;
    float vyDis;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
    
    
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_scale);
        //调整seekbar,设置硬度范围
        stiffness=(SeekBar) findViewById(R.id.stiffness);
        stiffness.setMax((int) SpringForce.STIFFNESS_HIGH);
        stiffness.setProgress((int) SpringForce.STIFFNESS_VERY_LOW);
        //设置阻尼范围,因为阻尼系数范围是0.0-1.0,所以乘以1000可视化为seekbar,转化为整数
        damping=(SeekBar) findViewById(R.id.damping);
        //damping.setMax((int) (SpringForce.DAMPING_RATIO_NO_BOUNCY*1000));
        damping.setProgress((int) (SpringForce.DAMPING_RATIO_HIGH_BOUNCY*1000));

        sView=findViewById(R.id.SView);
        sView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
    
    
            @Override
            public void onGlobalLayout() {
    
    
                vxDis=sView.getRight()-sView.getLeft();
                vyDis=sView.getBottom()-sView.getTop();
                sView.setPivotX(vxDis/2);
                sView.setPivotY(vyDis/2);
                xAnimation=createSpringAnimation(sView,SpringAnimation.SCALE_X,sView.getScaleX(),getStiffness(),getDamp());
                yAnimation=createSpringAnimation(sView,SpringAnimation.SCALE_Y,sView.getScaleY(),getStiffness(),getDamp());
                sView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
            }
        });//确定初始位置,确定后移除监听器
        sView.setOnTouchListener(new View.OnTouchListener() {
    
    
            @Override
            public boolean onTouch(View v, MotionEvent event) {
    
    
                switch (event.getActionMasked()){
    
    
                    case MotionEvent.ACTION_DOWN:

                        sX=vxDis/2-event.getRawX();//计算鼠标到当前视图中心的距离
                        sY=vyDis/2-event.getRawY();
                        xAnimation.getSpring().setStiffness(getStiffness());
                        yAnimation.getSpring().setStiffness(getStiffness());
                        xAnimation.getSpring().setDampingRatio(getDamp());
                        yAnimation.getSpring().setDampingRatio(getDamp());
//                        xAnimation.cancel();
//                        yAnimation.cancel();
                        break;
                    case MotionEvent.ACTION_MOVE:
                        if((vxDis/2-event.getRawX())/sX >= 0 && (vyDis/2-event.getRawY())/sY >= 0){
    
    
                            sView.animate()
                                    .scaleX((vxDis/2-event.getRawX())/sX)
                                    .scaleY((vyDis/2-event.getRawY())/sY)
                                    .setDuration(0)
                                    .start();
                        }
                        break;
                    case MotionEvent.ACTION_UP:
                        xAnimation.getSpring().setFinalPosition(1);
                        yAnimation.getSpring().setFinalPosition(1);
                        xAnimation.start();
                        yAnimation.start();
                        break;
                    default:
                        break;
                }
                return true;
            }
        });
    }

    @SuppressLint("Range")
    SpringAnimation createSpringAnimation(View view,
                                          DynamicAnimation.ViewProperty property,
                                          Float scaleChange,
                                          @FloatRange(from = 0.0) Float stiffness,
                                          @FloatRange(from = 0.0) Float dampingRatio) {
    
    
        //创建弹性动画类SpringAnimation
        SpringAnimation animation = new SpringAnimation(view, property);
        //SpringForce类,定义弹性特质
        SpringForce spring = new SpringForce(scaleChange);
        spring.setStiffness(stiffness);//
        spring.setDampingRatio(dampingRatio);
        //关联弹性特质
        animation.setSpring(spring);
        return animation;
    }
    private float getStiffness(){
    
    
        return Math.max(stiffness.getProgress(),1f);
    }
    private float getDamp(){
    
    
        return damping.getProgress()/1000f;
    }
}

activity_scale.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".ScaleActivity"
    android:orientation="vertical">

    <ImageView
        android:id="@+id/SView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:scaleType="centerCrop"
        android:layout_marginTop="20dp"
        android:src="@drawable/img0"
        tools:ignore="ContentDescription"/>
<!--    android:layout_gravity="center"-->
    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="center"
        android:layout_marginTop="20dp"
        android:text="stiffness"
        android:textSize="9sp"/>

    <SeekBar
        android:id="@+id/stiffness"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:maxHeight="20dp"
        android:minHeight="20dp"
        android:thumb="@drawable/ic_tag"
        android:progressTint="#CE3BF4" />

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="center"
        android:text="damping"
        android:textSize="9sp"/>

    <SeekBar
        android:id="@+id/damping"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@color/white"
        android:maxHeight="20dp"
        android:minHeight="20dp"
        android:thumb="@drawable/ic_tag"
        android:progressTint="#F4811C"
        />

</LinearLayout>

Demo:
s

7.MainActivity

package com.example.mydef20;

import androidx.annotation.FloatRange;
import androidx.appcompat.app.AppCompatActivity;
import androidx.dynamicanimation.animation.DynamicAnimation;
import androidx.dynamicanimation.animation.SpringAnimation;
import androidx.dynamicanimation.animation.SpringForce;

import android.annotation.SuppressLint;
import android.content.Intent;
import android.os.Bundle;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewTreeObserver;
import android.widget.Button;

public class MainActivity extends AppCompatActivity {
    
    
    private Button poButton;
    private Button roButton;
    private Button scButton;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
    
    
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        poButton=(Button) findViewById(R.id.positionButton);
        roButton=(Button) findViewById(R.id.rotationButton);
        scButton=(Button) findViewById(R.id.scaleButton);
        poButton.setOnClickListener(new View.OnClickListener() {
    
    
            @Override
            public void onClick(View v) {
    
    
                Intent intent=new Intent();
                intent.setClass(MainActivity.this,PositionActivity.class);
                startActivity(intent);
            }
        });
        roButton.setOnClickListener(new View.OnClickListener() {
    
    
            @Override
            public void onClick(View v) {
    
    
                Intent intent=new Intent();
                intent.setClass(MainActivity.this,RotationActivity.class);
                startActivity(intent);
            }
        });
        scButton.setOnClickListener(new View.OnClickListener() {
    
    
            @Override
            public void onClick(View v) {
    
    
                Intent intent=new Intent();
                intent.setClass(MainActivity.this,ScaleActivity.class);
                startActivity(intent);
            }
        });
    }
}

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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"
    android:orientation="vertical">
    <Button
        android:id="@+id/positionButton"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Position"
        android:textAllCaps="false"/>
    <Button
        android:id="@+id/rotationButton"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Rotation"
        android:textAllCaps="false"/>
    <Button
        android:id="@+id/scaleButton"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Scale"
        android:textAllCaps="false"/>

</LinearLayout>

my source code

csdn download link: CSDN download link

GitHub: github link

Please point out any problems. If you are new to Android, you can recommend any books in the comment area, thank you!

Guess you like

Origin blog.csdn.net/qq_43738932/article/details/119537507