Android实现仿网易云音乐黑胶唱片效果
前言:
网易云音乐的黑胶唱片效果一直是很多音乐迷的最爱,之前实现过此效果,今天总结一下,直接上代码:
1.自定义圆形ImageView:
package com.example.vinylrecordplayer.view
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.*
import android.util.AttributeSet
import androidx.appcompat.widget.AppCompatImageView
import com.example.vinylrecordplayer.R
/**
* @author: njb
* @date: 2023/5/21 22:56
* @desc:
*/
@Suppress("NAME_SHADOWING")
class RoundImageView : AppCompatImageView {
private val paint: Paint by lazy { Paint() }
private var roundWidth = 10
private var roundHeight = 10
private val paint2: Paint by lazy { Paint() }
constructor(context: Context, attrs: AttributeSet?, defStyle: Int) : super(
context,
attrs,
defStyle
) {
init(context, attrs)
}
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
init(context, attrs)
}
constructor(context: Context) : super(context) {
init(context, null)
}
@SuppressLint("Recycle")
private fun init(context: Context, attrs: AttributeSet?) {
if (attrs != null) {
val attrs = context.obtainStyledAttributes(attrs, R.styleable.RoundImageView)
roundWidth = attrs.getDimensionPixelSize(R.styleable.RoundImageView_x_radius, roundWidth)
roundHeight = attrs.getDimensionPixelSize(R.styleable.RoundImageView_y_radius, roundHeight)
} else {
val density = context.resources.displayMetrics.density
roundWidth = (roundWidth * density).toInt()
roundHeight = (roundHeight * density).toInt()
}
paint.color = Color.WHITE
paint.isAntiAlias = true
paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.DST_OUT)
}
override fun draw(canvas: Canvas) {
val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
val canvas2 = Canvas(bitmap)
super.draw(canvas2)
drawLiftUp(canvas2)
drawLiftDown(canvas2)
drawRightUp(canvas2)
drawRightDown(canvas2)
canvas.drawBitmap(bitmap, 0f, 0f, paint2)
bitmap.recycle()
}
private fun drawLiftUp(canvas: Canvas) {
val path = Path()
path.moveTo(0f, roundHeight.toFloat())
path.lineTo(0f, 0f)
path.lineTo(roundWidth.toFloat(), 0f)
path.arcTo(
RectF(0f, 0f, (roundWidth * 2).toFloat(), (roundHeight * 2).toFloat()),
-90f,
-90f
)
path.close()
canvas.drawPath(path, paint)
}
private fun drawLiftDown(canvas: Canvas) {
val path = Path()
path.moveTo(0f, (height - roundHeight).toFloat())
path.lineTo(0f, height.toFloat())
path.lineTo(roundWidth.toFloat(), height.toFloat())
path.arcTo(
RectF(
0f,
(height - roundHeight * 2).toFloat(),
(roundWidth * 2).toFloat(),
height.toFloat()
), 90f, 90f
)
path.close()
canvas.drawPath(path, paint)
}
private fun drawRightDown(canvas: Canvas) {
val path = Path()
path.moveTo((width - roundWidth).toFloat(), height.toFloat())
path.lineTo(width.toFloat(), height.toFloat())
path.lineTo(width.toFloat(), (height - roundHeight).toFloat())
path.arcTo(
RectF(
(width - roundWidth * 2).toFloat(),
(height - roundHeight * 2).toFloat(),
width.toFloat(),
height.toFloat()
), -0f, 90f
)
path.close()
canvas.drawPath(path, paint)
}
private fun drawRightUp(canvas: Canvas) {
val path = Path()
path.moveTo(width.toFloat(), roundHeight.toFloat())
path.lineTo(width.toFloat(), 0f)
path.lineTo((width - roundWidth).toFloat(), 0f)
path.arcTo(
RectF(
(width - roundWidth * 2).toFloat(),
0f,
width.toFloat(),
(roundHeight * 2).toFloat()
), -90f, 90f
)
path.close()
canvas.drawPath(path, paint)
}
}
2.自定义悬浮窗按钮:
package com.example.vinylrecordplayer.view
import android.content.Context
import android.util.AttributeSet
import android.view.Gravity
import android.view.MotionEvent
import android.view.View
import android.widget.LinearLayout
import com.example.vinylrecordplayer.R
import com.example.vinylrecordplayer.utils.DisplayUtils
import com.example.vinylrecordplayer.utils.DisplayUtils.dip2px
import kotlinx.android.synthetic.main.play_music_rotate.view.*
import kotlin.math.abs
/**
* des 自定义音乐悬浮按钮
* @date 2020/6/28
* @author njb
*/
class FloatPlayLayout : LinearLayout {
/**
* content应该展示的宽度
*/
private val contentWidth = dip2px(context, 180f)
constructor(context: Context) : super(context) {
initView(context)
}
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
initView(context)
}
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(
context,
attrs,
defStyleAttr
) {
initView(context)
}
override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
when (ev.action) {
MotionEvent.ACTION_DOWN -> {
moveY = ev.y
}
MotionEvent.ACTION_MOVE -> {
//兼容某些机型一碰就触发滑动
if (abs(ev.y - moveY) > 100) {
return true
}
}
}
return false
}
private var moveY = 0f
override fun onTouchEvent(event: MotionEvent): Boolean {
when (event.action) {
MotionEvent.ACTION_MOVE -> {
var offsetY = translationY + (event.y - moveY)
//因为translationY不管处于ViewGroup什么位置,初始值都为0,所以要买个top
//最小值
if (offsetY < -top + dip2px(context, 100f)) {
offsetY = -top.toFloat() + dip2px(context, 100f)
}
//最大值
if (offsetY > DisplayUtils.getScreenHeight(context) - top - dip2px(context, 150f)) {
offsetY = DisplayUtils.getScreenHeight(context) - top.toFloat() - dip2px(
context,
150f
)
}
translationY = offsetY
return true
}
}
return super.onInterceptTouchEvent(event)
}
private fun initView(context: Context) {
View.inflate(context, R.layout.play_music_rotate, this)
//居中显示
gravity = Gravity.CENTER
//设置阴影
click()
}
/**
* 事件
*/
private fun click() {
}
/**
* 悬浮窗点击事件
*/
fun rootClick(onClick: (View) -> Unit) {
anchorLiveMusic.clickNoRepeat {
onClick.invoke(it)
}
}
}
/**
* 防止重复点击
* @param interval 重复间隔
* @param onClick 事件响应
*/
var lastTime = 0L
fun View.clickNoRepeat(interval: Long = 400, onClick: (View) -> Unit) {
setOnClickListener {
val currentTime = System.currentTimeMillis()
if (lastTime != 0L && (currentTime - lastTime < interval)) {
return@setOnClickListener
}
lastTime = currentTime
onClick(it)
}
}
3.自定义旋转的唱片view:
package com.example.vinylrecordplayer.view;
import android.animation.ObjectAnimator;
import android.animation.ValueAnimator;
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.animation.LinearInterpolator;
import android.widget.FrameLayout;
import android.widget.ImageView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.example.vinylrecordplayer.R;
/**
* @author: njb
* @date: 2021/5/25 16:34
* @desc: 描述
*/
public class RotateAlbumView extends FrameLayout {
private static final String TAG = "RotateAlbumView";
private ImageView ivAlbumPic;
private ObjectAnimator animator;
public RotateAlbumView(@NonNull Context context) {
this(context, null);
}
public RotateAlbumView(@NonNull Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public RotateAlbumView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}
// 设置旋转动画(属性动画)
private void init(Context context) {
View.inflate(context, R.layout.view_rotate_album, this);
ivAlbumPic = (ImageView) findViewById(R.id.view_pic);
animator = ObjectAnimator.ofFloat(ivAlbumPic, "rotation", 0.0F, 360.0F);
animator.setDuration(5 * 1000);
animator.setInterpolator(new LinearInterpolator());
animator.setRepeatCount(ObjectAnimator.INFINITE);
animator.setRepeatMode(ValueAnimator.RESTART);
setPlaying(true);
}
// 更新播放状态
public void setPlaying(boolean isPlaying) {
Log.d(TAG, "update RotateAlbumView: isPlaying = " + isPlaying);
if (isPlaying) {
if (!animator.isRunning()) {
animator.start();
} else {
animator.resume();
}
} else {
if (!animator.isStarted() || !animator.isRunning()) {
animator.cancel();
}
animator.pause();
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
return super.onTouchEvent(event);
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
Log.d(TAG, "RotateAlbumView: onDetachedFromWindow");
animator.cancel();
}
}
4.自定义黑胶效果:
package com.example.vinylrecordplayer.view;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.SweepGradient;
import android.util.AttributeSet;
import android.view.View;
import androidx.annotation.ColorInt;
import androidx.annotation.ColorRes;
import androidx.annotation.Nullable;
import com.example.vinylrecordplayer.R;
/**
* @author: njb
* @date: 2021/5/25 16:35
* @desc: 描述
*/
public class VinylRecordView extends View {
private Paint paint;
// 圆环半径
private int ringWidth;
// 渐变色
private int[] colors;
private SweepGradient gradient;
// 圆线距圆环内边的距离
private int[] ringLinesMarginOut = {
dp2px(3.78F),
dp2px(7.03F),
dp2px(10.27F),
dp2px(12.97F)
};
// 圆线高度
private int ringLineWidth;
public VinylRecordView(Context context) {
this(context, null);
}
public VinylRecordView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public VinylRecordView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context, attrs);
}
private void init(Context context, AttributeSet attrs) {
paint = new Paint();
paint.setAntiAlias(true);
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeCap(Paint.Cap.ROUND);
colors = new int[]{getColor(R.color.widget_album_ring_color1), getColor(R.color.widget_album_ring_color2),
getColor(R.color.widget_album_ring_color1), getColor(R.color.widget_album_ring_color2),
getColor(R.color.widget_album_ring_color1)};
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.VinylRecordView);
ringWidth = (int) typedArray.getDimension(R.styleable.VinylRecordView_ring_width, getResources().getDimension(R.dimen.widget_album_ring_width));
ringLineWidth = (int) typedArray.getDimension(R.styleable.VinylRecordView_ring_line_width, getResources().getDimension(R.dimen.widget_album_ring_line_width));
typedArray.recycle();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
paint.setStrokeWidth(ringWidth);
paint.setColor(getColor(R.color.widget_album_ring_color1));
if (gradient == null) {
gradient = new SweepGradient(getWidth() * 0.5F, getHeight() * 0.5F, colors, new float[]{
0F, 0.25F, 0.5F, 0.75F, 1F
});
}
paint.setShader(gradient);
canvas.drawCircle(getWidth() * 0.5F, getHeight() * 0.5F, (getWidth() - ringWidth) * 0.5F, paint);
paint.setShader(null);
paint.setStrokeWidth(ringLineWidth);
paint.setColor(getColor(R.color.widget_album_ring_line_color));
for (int marginOut : ringLinesMarginOut) {
canvas.drawCircle(getWidth() * 0.5F, getHeight() * 0.5F, getWidth() * 0.5F - marginOut - ringLineWidth * 0.5F, paint);
}
}
private int dp2px(float dp) {
float scale = getResources().getDisplayMetrics().density;
return (int) (dp * scale + 0.5F);
}
@ColorInt
private int getColor(@ColorRes int colorId) {
return getResources().getColor(colorId, null);
}
}
5.MainActivity使用:
package com.example.vinylrecordplayer
import android.os.Bundle
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import kotlinx.android.synthetic.main.activity_main.*
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
initView()
}
private fun initView() {
floatMusic.rootClick {
Toast.makeText(this@MainActivity,"点击了悬浮窗",Toast.LENGTH_SHORT).show()
}
}
}
6.主界面代码:
<?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.example.vinylrecordplayer.view.FloatPlayLayout
android:id="@+id/floatMusic"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>