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_ANIAMTOR(500毫秒); * 3. 设置动画的插值器为AccelerateInterpolator,实现加速的动画效果; * 4. 为唱针动画添加AnimatorListener监听器,在动画开始、结束、取消和重复时执行相应的逻辑处理,例如根据动画开始前的唱针状态来确定动画进行时的状态, * 在动画结束时根据唱针状态控制唱片动画的播放、暂停以及处理后续是否需要延迟启动唱盘旋转动画等逻辑。 */ private void initObjectAnimator() { // 创建一个ObjectAnimator对象,用于控制唱针的旋转动画,指定动画作用的视图(mIvNeedle)、动画属性(View.ROTATION表示旋转角度)以及起始和结束的角度值。 mNeedleAnimator = ObjectAnimator.ofFloat(mIvNeedle, View.ROTATION, DisplayUtil .ROTATION_INIT_NEEDLE, 0); // 设置唱针动画的持续时间,这里使用之前定义好的常量DURATION_NEEDLE_ANIAMTOR(500毫秒),确定动画完成一次旋转的时间长度。 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) { } }); } /** * 获取合成后的唱片Drawable,该Drawable由空心圆盘和音乐专辑图片组成,具体实现步骤如下: * 1. 根据屏幕宽度以及定义好的比例(DisplayUtil.SCALE_DISC_SIZE和DisplayUtil.SCALE_MUSIC_PIC_SIZE)计算唱片和音乐专辑图片的显示尺寸; * 2. 分别对空心圆盘的Bitmap和音乐专辑的Bitmap进行缩放处理,使其大小符合计算后的尺寸要求