You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

288 lines
17 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

package com.example.musicplayer.widget;
import android.animation.Animator;
import android.animation.ObjectAnimator;
import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.LayerDrawable;
import android.support.v4.graphics.drawable.RoundedBitmapDrawable;
import android.support.v4.graphics.drawable.RoundedBitmapDrawableFactory;
import android.util.AttributeSet;
import android.view.View;
import android.view.animation.AccelerateInterpolator;
import android.view.animation.LinearInterpolator;
import android.widget.ImageView;
import android.widget.RelativeLayout;
import com.example.musicplayer.R;
import com.example.musicplayer.util.CommonUtil;
import com.example.musicplayer.util.DisplayUtil;
/**
* Created by 残渊 on 2018/10/27.
* 这个类继承自RelativeLayout用于创建一个自定义的视图实现类似音乐播放器中唱片和唱针的动画效果以及音乐播放状态控制等功能。
*/
public class DiscView extends RelativeLayout {
// 用于显示唱针的ImageView代表界面上的唱针元素
private ImageView mIvNeedle;
// 用于控制唱针旋转动画的ObjectAnimator对象通过它来操作唱针的旋转动画效果
private ObjectAnimator mNeedleAnimator;
// 用于控制唱片旋转动画的ObjectAnimator对象用于实现唱片的旋转动画
private ObjectAnimator mObjectAnimator;
/*
标记ViewPager是否处于偏移的状态用于判断在某些特定情况下是否执行相关动画逻辑
例如在ViewPager偏移时可能不启动唱盘旋转动画等情况。
*/
private boolean mViewPagerIsOffset = false;
/*
标记唱针复位后,是否需要重新偏移到唱片处,用于协调唱针动画和唱片动画之间的启动顺序和逻辑,
比如在特定条件下确定是否要延迟启动唱盘旋转动画等情况。
*/
private boolean mIsNeed2StartPlayAnimator = false;
// 表示音乐当前的状态初始化为MusicStatus.STOP有播放PLAY、暂停PAUSE、停止STOP三种可能状态。
private MusicStatus musicStatus = MusicStatus.STOP;
// 定义唱针动画的持续时间单位为毫秒这里设置为500毫秒表示唱针旋转动画完成一次的时间长度。
public static final int DURATION_NEEDLE_ANIAMTOR = 500;
// 唱针当前所处的状态初始化为NeedleAnimatorStatus.IN_FAR_END即初始状态下唱针处于远离唱片的位置。
private NeedleAnimatorStatus needleAnimatorStatus = NeedleAnimatorStatus.IN_FAR_END;
// 存储屏幕的宽度通过CommonUtil工具类获取用于后续根据屏幕尺寸来设置各种视图元素的大小和位置等。
private int mScreenWidth;
// 存储屏幕的高度同样通过CommonUtil工具类获取作用与屏幕宽度类似辅助布局相关的计算。
private int mScreenHeight;
/*
定义唱针当前所处的状态的枚举类型,包含以下几种情况:
TO_FAR_END表示唱针正在从唱盘往远处移动的过程中
TO_NEAR_END表示唱针正在从远处往唱盘移动的过程中
IN_FAR_END表示唱针静止时处于离开唱盘的位置
IN_NEAR_END表示唱针静止时处于贴近唱盘的位置。
用于清晰地表示和管理唱针在不同时刻的位置状态,以便根据状态来控制相应动画逻辑。
*/
private enum NeedleAnimatorStatus {
/*移动时:从唱盘往远处移动*/
TO_FAR_END,
/*移动时:从远处往唱盘移动*/
TO_NEAR_END,
/*静止时:离开唱盘*/
IN_FAR_END,
/*静止时:贴近唱盘*/
IN_NEAR_END
}
/*
定义音乐当前的状态的枚举类型,明确只有三种状态:
PLAY表示音乐正在播放
PAUSE表示音乐处于暂停状态
STOP表示音乐处于停止状态。
方便统一管理和判断音乐的播放情况,在不同方法中根据此状态来执行相应操作。
*/
public enum MusicStatus {
PLAY, PAUSE, STOP
}
// 构造函数调用带有两个参数的构造函数并传入null作为AttributeSet参数这是一种常见的构造函数重载调用方式。
public DiscView(Context context) {
this(context, null);
}
// 构造函数调用带有三个参数的构造函数并传入0作为defStyleAttr参数用于初始化视图同时传递上下文和属性集信息。
public DiscView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
/**
* 主要的构造函数,接收上下文、属性集和样式属性参数,调用父类的构造函数完成初始化,
* 并获取屏幕的宽度和高度,这两个尺寸信息后续会用于视图布局和元素大小设置等操作。
* @param context 上下文环境,用于获取系统资源等操作。
* @param attrs 属性集用于解析在XML布局中定义的该视图的属性信息可为null。
* @param defStyleAttr 样式属性用于指定默认的样式主题相关信息这里传入0表示使用默认值。
*/
public DiscView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mScreenWidth = CommonUtil.getScreenWidth(context);
mScreenHeight = CommonUtil.getScreenHeight(context);
}
/**
* 在视图从XML布局文件中加载完成后会调用此方法用于执行一些初始化操作
* 比如初始化唱片图片、唱针相关设置以及动画相关的初始化等工作,确保视图展示所需的各种元素和效果准备就绪。
*/
@Override
protected void onFinishInflate() {
super.onFinishInflate();
initDiscImg();
initNeedle();
initObjectAnimator();
}
/**
* 初始化唱片相关的图片显示及布局参数设置,主要完成以下工作:
* 1. 找到代表唱片背景的ImageView
* 2. 获取控制唱片旋转的ObjectAnimator实例
* 3. 设置唱片背景的Drawable这个Drawable是由空心圆盘和音乐专辑图片合成得到的
* 4. 根据屏幕高度计算唱片在布局中的上边距,以调整唱片在界面上的显示位置。
*/
private void initDiscImg() {
// 通过findViewById方法在当前布局中查找id为iv_disc_background的ImageView它代表唱片的背景元素。
ImageView mDiscBackground = findViewById(R.id.iv_disc_background);
// 获取用于控制唱片旋转动画的ObjectAnimator对象传入唱片背景的ImageView作为参数以便后续操作该视图的旋转动画。
mObjectAnimator = getDiscObjectAnimator(mDiscBackground);
// 设置唱片背景的Drawable通过调用getDiscDrawable方法获取合成后的Drawable包含专辑图片和圆盘图片
mDiscBackground.setImageDrawable(getDiscDrawable(
BitmapFactory.decodeResource(getResources(), R.drawable.default_disc)
));
// 根据屏幕高度以及定义好的比例DisplayUtil.SCALE_DISC_MARGIN_TOP计算唱片的上边距用于调整唱片在布局中的垂直位置。
int marginTop = (int) (DisplayUtil.SCALE_DISC_MARGIN_TOP * mScreenHeight);
LayoutParams layoutParams = (LayoutParams) mDiscBackground
.getLayoutParams();
// 设置唱片背景ImageView的外边距将上边距设置为计算得到的值左右和下边距设置为0从而定位唱片在布局中的位置。
layoutParams.setMargins(0, marginTop, 0, 0);
mDiscBackground.setLayoutParams(layoutParams);
}
/**
* 初始化唱针相关的属性设置,包括唱针的大小、位置、旋转角度以及对应的布局参数等内容,具体如下:
* 1. 通过findViewById找到代表唱针的ImageView
* 2. 根据屏幕宽度和高度以及定义好的比例各种SCALE_*常量)计算唱针的宽度和高度,使其适配不同屏幕尺寸;
* 3. 通过设置外边距(部分外边距为负数)来隐藏唱针的一部分,实现特定的显示效果;
* 4. 对唱针的原始Bitmap进行缩放处理使其大小符合计算后的尺寸
* 5. 设置唱针旋转的中心点坐标(基于屏幕宽度的比例计算);
* 6. 设置唱针的初始旋转角度并将处理后的Bitmap设置给唱针的ImageView最后更新其布局参数。
*/
private void initNeedle() {
// 通过findViewById方法在当前布局中查找id为iv_needle的ImageView它代表唱针元素。
mIvNeedle = findViewById(R.id.iv_needle);
// 根据屏幕宽度以及定义好的比例DisplayUtil.SCALE_NEEDLE_WIDTH计算唱针的宽度使其在不同屏幕上显示合适大小。
int needleWidth = (int) (DisplayUtil.SCALE_NEEDLE_WIDTH * mScreenWidth);
// 根据屏幕高度以及定义好的比例DisplayUtil.SCALE_NEEDLE_HEIGHT计算唱针的高度同样用于适配屏幕尺寸。
int needleHeight = (int) (DisplayUtil.SCALE_NEEDLE_HEIGHT * mScreenHeight);
/*
设置唱针手柄的外边距通过将上边距设置为负数基于屏幕高度和定义好的比例DisplayUtil.SCALE_NEEDLE_MARGIN_TOP计算
让唱针的一部分隐藏起来,达到特定的视觉效果。
*/
int marginTop = (int) (DisplayUtil.SCALE_NEEDLE_MARGIN_TOP * mScreenHeight) * -1;
// 根据屏幕宽度以及定义好的比例DisplayUtil.SCALE_NEEDLE_MARGIN_LEFT计算唱针的左边距确定其水平位置。
int marginLeft = (int) (DisplayUtil.SCALE_NEEDLE_MARGIN_LEFT * mScreenWidth);
// 从资源文件R.drawable.ic_needle中获取唱针的原始Bitmap对象用于后续的处理和显示。
Bitmap originBitmap = BitmapFactory.decodeResource(getResources(), R.drawable
.ic_needle);
// 根据计算得到的宽度和高度对原始唱针Bitmap进行缩放处理创建一个新的缩放后的Bitmap用于设置给唱针的ImageView。
Bitmap bitmap = Bitmap.createScaledBitmap(originBitmap, needleWidth, needleHeight, false);
LayoutParams layoutParams = (LayoutParams) mIvNeedle.getLayoutParams();
// 设置唱针ImageView的外边距将左边距和上边距设置为计算得到的值右边和下边距设置为0确定唱针在布局中的位置。
layoutParams.setMargins(marginLeft, marginTop, 0, 0);
// 根据屏幕宽度以及定义好的比例DisplayUtil.SCALE_NEEDLE_PIVOT_X计算唱针旋转的中心点X坐标用于确定旋转中心位置。
int pivotX = (int) (DisplayUtil.SCALE_NEEDLE_PIVOT_X * mScreenWidth);
// 根据屏幕宽度以及定义好的比例DisplayUtil.SCALE_NEEDLE_PIVOT_Y计算唱针旋转的中心点Y坐标同样用于定位旋转中心。
int pivotY = (int) (DisplayUtil.SCALE_NEEDLE_PIVOT_Y * mScreenWidth);
// 设置唱针ImageView的旋转中心点的X坐标使其绕该点进行旋转动画操作。
mIvNeedle.setPivotX(pivotX);
// 设置唱针ImageView的旋转中心点的Y坐标与X坐标共同确定旋转中心位置。
mIvNeedle.setPivotY(pivotY);
// 设置唱针的初始旋转角度角度值由DisplayUtil.ROTATION_INIT_NEEDLE定义一般为远离唱片的初始角度。
mIvNeedle.setRotation(DisplayUtil.ROTATION_INIT_NEEDLE);
// 将缩放后的唱针Bitmap设置给唱针的ImageView使其显示在界面上。
mIvNeedle.setImageBitmap(bitmap);
// 更新唱针ImageView的布局参数使设置的外边距、旋转中心点等属性生效完成唱针的初始化布局设置。
mIvNeedle.setLayoutParams(layoutParams);
}
/**
* 初始化唱针的动画相关设置,包括以下关键操作:
* 1. 创建一个ObjectAnimator对象用于控制唱针的旋转动画设置旋转角度从初始角度DisplayUtil.ROTATION_INIT_NEEDLE到0度贴近唱片的角度变化
* 2. 设置唱针动画的持续时间为DURATION_NEEDLE_ANIAMTOR500毫秒
* 3. 设置动画的插值器为AccelerateInterpolator实现加速的动画效果
* 4. 为唱针动画添加AnimatorListener监听器在动画开始、结束、取消和重复时执行相应的逻辑处理例如根据动画开始前的唱针状态来确定动画进行时的状态
* 在动画结束时根据唱针状态控制唱片动画的播放、暂停以及处理后续是否需要延迟启动唱盘旋转动画等逻辑。
*/
private void initObjectAnimator() {
// 创建一个ObjectAnimator对象用于控制唱针的旋转动画指定动画作用的视图mIvNeedle、动画属性View.ROTATION表示旋转角度以及起始和结束的角度值。
mNeedleAnimator = ObjectAnimator.ofFloat(mIvNeedle, View.ROTATION, DisplayUtil
.ROTATION_INIT_NEEDLE, 0);
// 设置唱针动画的持续时间这里使用之前定义好的常量DURATION_NEEDLE_ANIAMTOR500毫秒确定动画完成一次旋转的时间长度。
mNeedleAnimator.setDuration(DURATION_NEEDLE_ANIAMTOR);
// 设置唱针动画的插值器为AccelerateInterpolator使得唱针在旋转过程中呈现加速的动画效果增强视觉上的真实感。
mNeedleAnimator.setInterpolator(new AccelerateInterpolator());
mNeedleAnimator.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animator) {
/**
* 根据动画开始前NeedleAnimatorStatus唱针当前所处的状态的状态
* 来确定动画进行时NeedleAnimatorStatus的状态以便后续根据状态进行相应的逻辑处理。
* 例如如果开始前唱针处于远离唱片IN_FAR_END状态那么动画进行时就是往唱片方向移动TO_NEAR_END
* 如果开始前唱针处于贴近唱片IN_NEAR_END状态动画进行时就是往远离唱片方向移动TO_FAR_END
*/
if (needleAnimatorStatus == NeedleAnimatorStatus.IN_FAR_END) {
needleAnimatorStatus = NeedleAnimatorStatus.TO_NEAR_END;
} else if (needleAnimatorStatus == NeedleAnimatorStatus.IN_NEAR_END) {
needleAnimatorStatus = NeedleAnimatorStatus.TO_FAR_END;
}
}
@Override
public void onAnimationEnd(Animator animator) {
if (needleAnimatorStatus == NeedleAnimatorStatus.TO_NEAR_END) {
needleAnimatorStatus = NeedleAnimatorStatus.IN_NEAR_END;
playDiscAnimator();
musicStatus = MusicStatus.PLAY;
} else if (needleAnimatorStatus == NeedleAnimatorStatus.TO_FAR_END) {
needleAnimatorStatus = NeedleAnimatorStatus.IN_FAR_END;
if (musicStatus == MusicStatus.STOP) {
mIsNeed2StartPlayAnimator = true;
}
}
if (mIsNeed2StartPlayAnimator) {
mIsNeed2StartPlayAnimator = false;
/**
* 只有在ViewPager不处于偏移状态mViewPagerIsOffset为false才开始唱盘旋转动画
* 否则需要等待合适的时机比如ViewPager回到正常位置再启动唱盘动画通过延迟50毫秒后调用playAnimator方法来实现。
* 这里的延迟是为了确保相关状态和条件准备就绪,避免出现动画冲突或不符合预期的显示效果。
*/
if (!mViewPagerIsOffset) {
/*延时500ms*/
DiscView.this.postDelayed(new Runnable() {
@Override
public void run() {
playAnimator();
}
}, 50);
}
}
}
@Override
public void onAnimationCancel(Animator animator) {
}
@Override
public void onAnimationRepeat(Animator animator) {
}
});
}
/**
* DrawableDrawable
* 1. DisplayUtil.SCALE_DISC_SIZEDisplayUtil.SCALE_MUSIC_PIC_SIZE
* 2. BitmapBitmap使