游戏界面实现
AndroidManifest.xml文件里设置该页面为无顶部标题栏的样式
<activity
android:name=".GameActivity"
android:theme="@style/Theme.AppCompat.Light.NoActionBar" />
activity_game.xml
利用android:layout_weight="1"实现底部按钮均分
<?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=".GameActivity"
android:orientation="vertical"
android:id="@+id/buttonConstraintLayout"
android:background="@drawable/b_bottom"
>
<!--右上角实时得分视图-->
<TextView
android:id="@+id/score"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginRight="20dp"
android:layout_marginTop="20dp"
android:text="Score:0"
android:textColor="@color/colorWhite"
android:textSize="20dp"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent">
</TextView>
<!--底部按钮,距顶部距离400dp-->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="400dp"
android:orientation="horizontal"
app:layout_constraintTop_toTopOf="parent"
>
<!--利用android:layout_weight="1"实现均分-->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center"
>
<Button
android:layout_width="120dp"
android:layout_height="60dp"
android:background="@color/colorLightYellow"
android:text="FREE"
android:textSize="18dp"
android:id="@+id/bottom1"
></Button>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center"
>
<Button
android:layout_width="120dp"
android:layout_height="60dp"
android:background="@color/colorLightYellow"
android:text="FREE"
android:textSize="18dp"
android:id="@+id/bottom2"
></Button>
</LinearLayout>
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
GameActivity
package com.example.free;
import androidx.constraintlayout.widget.ConstraintLayout;
import android.animation.ObjectAnimator;
import android.content.Context;
import android.content.Intent;
import android.graphics.Color;
import android.media.MediaPlayer;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;
import com.example.free.Classes.BaseActivity;
import com.example.free.Classes.EachButton;
import com.example.free.Classes.HandleData;
import com.example.free.Mp3Handle.MyMp3FileReader;
import com.example.free.WavHandle.WaveFileReader;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Timer;
import java.util.TimerTask;
public class GameActivity extends BaseActivity {
public static boolean activityState=true;//活动状态,方便EachButton在buttons个数为0时判断是否要调往resultActivity(EachButton类内部,最后一个按钮时miss时跳转成绩页面的情况)
final Context context=GameActivity.this;//上下文活动
String musicPath="";//音乐文件的路径
long musicSize=0;//音乐文件的大小
long musicTime=0;//音乐文件时长
int window=1024;//节奏点分析时的样本窗口大小
int windowSize=20;//节奏点分析时的取平均值时的左右样本窗口大小
double multiplier=1.5;//节奏点分析时的样本加权值
MediaPlayer mediaPlayer=new MediaPlayer();//音频播放器
ArrayList<Integer> allTime=new ArrayList<>();//从音频里得到的节奏点时间
Button[] bottoms=new Button[2];//底部的两个电机按钮
static ConstraintLayout buttonConstraintLayout;//按钮放置的视图
public static ArrayList<ArrayList<EachButton>> buttons=new ArrayList();//滑块们
int deviceWidthPx=1080;//1080/3dp 设备宽度像素值
int deviceWidthDp=360;//dp 设备宽度转化的dp值
double dp_px=3.0;//1dp=3px; 分辨率 dp px单位转换的因子
//这些都是以dp为指标,需要转化px的变量,因为画面得到滑块位置返回的是px值
int bottomWidth=120;//dp
int buttonWidth=80;//dp
int bottomHeight=60;//底部按钮的高度,也是最后决定good best等级的高度
int buttonHeight=40;
int[] X={70,200};//轨道的x坐标 //button 宽80dp bottom 120dp
int startY=0;
int marginTop=400;//底部滑块的y坐标(同activity_game.xml文件里的设定)
int endY=marginTop+bottomHeight;//距top400dp
int duration=1000;//滑块运动时间
int blocks=0;//滑块数量 0 代表是默认或者高级设置 1 少 2 多,在getTime()那里用到去配置
//压缩音频:滑块少:windowSize:15,multiplier:1;滑块多:windowSize:20,multiplier:5;
//无损音频:滑块少:windowSize:10,multiplier:5;滑块多:windowSize:10,multiplier:2;
int microChange=20;//因为人的听觉和看到滑块盗来的视觉的差异,判定miss good best放松一些指标,这个是滑块按钮位置差的误差调整值
public static Toast toast;//toast对象,miss good best显示所用
final int goodScore=15;//good分值 15分
final int bestScore=20;//best分值 20分
public static int goodTimes;//good次数
public static int bestTimes;//best次数
public static int missTimes;//miss次数
public static int resultScore;//实际得分
public static int wholeScore;//原本总得分
TextView scoreView;//右上角得分的视图
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_game);
//这里要重新初始化活动状态,不然第二次选歌后这里默认false,eachButton那里无法访问resultActivity
activityState=true;
goodTimes=0;
bestTimes=0;
missTimes=0;
resultScore=0;
wholeScore=0;
//初始化视图,dp转化为px,方便eachButton类的加载
initDpToPx();
initPxs();
//得到按钮放置的视图
buttonConstraintLayout=findViewById(R.id.buttonConstraintLayout);
//得到右上角分值的视图
scoreView=findViewById(R.id.score);
//得到会话传来的音乐文件的路径 模式 滑块数量等信息
getIntentData();
//初始化音乐播放器
initMediaPlayer();
//得到节奏点时间
getTime();//节奏点时间储存在了allTime里面
//实例化toast对象,确定它所在位置是当前上下文
toast=new Toast(context);
//初始化底部按钮事件
initBottom();
//初始化按钮
initButtons();
//播放音频(初始完按钮后再开始播放)
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(duration*(marginTop+buttonHeight)/(endY));//这里从第一个滑块到底部按钮那儿了再开始音频
}catch(InterruptedException e){
e.printStackTrace();
}
mediaPlayer.start();
}
}).start();
}
@Override
protected void onDestroy() {
super.onDestroy();
if(mediaPlayer!=null){
mediaPlayer.stop();//中间强制退出时,要关掉播放器
mediaPlayer.release();//释放播放器所占资源
}
activityState=false;
Intent intent=new Intent("force_offline");
sendBroadcast(intent);
}
}
调整dp到dx转换
原理:如HuaWei caz-al10 像素1920*1080 屏幕5英寸 139.6x69.7mm 5.5x2.74英寸。
屏幕像素密度441ppi(横像素/横英寸,或者 PPI = √(长度像素数² + 宽度像素数²) / 屏幕尺寸,本手机是第二个计算方法)
得到机型的dps值为 长pxs*160/ppi=1920*160/441 dp=696.6 dp 宽pxs*160/ppi=1080*160/441 dp=391.8 dp
1px为一个像素点,不建议被使用,不同的机型一个图片长宽像素点占比不同
ppi (pixels per inch):图像分辨率 (在图像中,每英寸所包含的像素数目)
dpi (dots per inch): 打印分辨率 (每英寸所能打印的点数,即打印精度)
dp:Density-independent pixels,以160PPI屏幕为标准,则1dp=1px,dp和px的换算公式 :
dp*ppi/160 = px。比如1dp x 320ppi/160 = 2px。
sp:Scale-independent pixels,它是安卓的字体单位,以160PPI屏幕为标准,当字体大小为 100%时, 1sp=1px。
sp 与 px 的换算公式:sp*ppi/160 = px
文字用sp,非文字用dp
public void initDpToPx(){
// 低密度 ldpi: 240x320 ppi 120 1dp=0.75px
// 中密度 mdpi: 320x480 ppi 160 1dp=1px
// 高密度 hdpi: 480x800 ppi 240 1dp=1.5px
// 超高密度 xhdpi: 720x1280 ppi 320 1dp=2px
// 超超高密度 xxhdpi:1080x1920 ppi 480 1dp=3px
deviceWidthPx=getWindowManager().getDefaultDisplay().getWidth();//得到屏幕宽度的像素值px
Map<Integer,Double> dpis=new HashMap<Integer, Double>();
dpis.put(240,0.75);
dpis.put(320,1.0);
dpis.put(480,1.5);
dpis.put(720,2.0);
dpis.put(1080,3.0);
if(dpis.containsKey(deviceWidthPx)){
dp_px=dpis.get(deviceWidthPx);
}
deviceWidthDp=(int)(deviceWidthPx/dp_px);
}
public void initPxs(){
//需要转为px的指标
endY=(int)(endY*dp_px);//endY受bottomHeight影响,所以第一个改
marginTop=(int)(marginTop*dp_px);
bottomHeight=(int)(bottomHeight*dp_px);
buttonHeight=(int)(buttonHeight*dp_px);
buttonWidth=(int)(buttonWidth*dp_px);
bottomWidth=(int)(bottomWidth*dp_px);
int temp=bottomWidth-buttonWidth;
for(int i=0;i<X.length;i++){
X[i]=deviceWidthPx/X.length*i+(deviceWidthPx/X.length-buttonWidth)/2;//轨道(最左点)横坐标(底部按钮已均分居中)
}
}
获得会话传来的音频信息和模式选择结果
public void getIntentData(){
Intent intent=getIntent();//得到会话
String className=intent.getStringExtra("className");
if(className.equals("ChoiceActivity")) {//自定义模式界面
musicPath = intent.getStringExtra("musicPath");//音乐文件路径
musicSize = intent.getLongExtra("musicSize", 0);//音乐文件大小
musicTime = intent.getLongExtra("musicTime", 0);//音乐文件时长
//getIntExtra getLongExtra需要有个默认参数值
window = intent.getIntExtra("window", 1024);//样本窗口大小
windowSize = intent.getIntExtra("windowSize", 20);//样本窗口左右窗口数
multiplier = intent.getDoubleExtra("multiplier", 3);//阈值加权值
duration=intent.getIntExtra("duration",1000);//滑块滑动动画的时长
}
else{
if(className.equals("OriginChoiceActivity")){//初始模式选择界面(少 多;慢 快)
blocks=intent.getIntExtra("blocks",1);//滑块多少 1少 2多
int rates=intent.getIntExtra("rates",1);//滑块速度 1慢 2中 3块
switch(rates){//越慢滑块滑动时间越长
case 1:duration=1200;break;
case 2:duration=1000;break;
case 3:duration=800;break;
default:
}
musicPath = intent.getStringExtra("musicPath");//音乐文件路径
musicSize = intent.getLongExtra("musicSize", 0);//音乐文件大小
musicTime = intent.getLongExtra("musicTime", 0);//音乐文件时长
}
}
}
初始化音频播放器
public void initMediaPlayer(){
try {
mediaPlayer.setDataSource(musicPath);//给播放器音乐文件的路径
mediaPlayer.prepare();//让mediaPlayer进入到准备状态
}catch(Exception e){
Toast.makeText(context,"no music file found",Toast.LENGTH_SHORT);
}
}
获得节奏点时间
public void getTime(){
int[] data=null;
if(musicPath.contains(".wav")){//如果是wav格式,则用WaveFileReader类去采样获得数据流
WaveFileReader reader = new WaveFileReader(musicPath);
data= reader.getData()[0];
if(blocks==1){//滑块少
windowSize=15;multiplier=2;
}
else{//滑块多
if(blocks==2){
windowSize=20;multiplier=5;
}
}
//通过HandleData类将节奏点时间赋值给allTime
HandleData.handleData(data, allTime, 16000, musicTime, window, windowSize,multiplier);
}
else if(musicPath.contains(".mp3")){//如果是mp3格式,则用MyMp3FileReader类去采样获得数据流
MyMp3FileReader reader = new MyMp3FileReader(musicPath);
data=reader.getData();
if(musicSize/musicTime>30) {//高音质 无损(无损音频和有损音频的格式等情况不太一致)
if(blocks==1){//无损,滑块少
windowSize=10;multiplier=5;
}
else{//无损,滑块多
if(blocks==2){
windowSize=10;multiplier=4;
}
}
HandleData.handleData(data, allTime, 24000, musicTime, window, windowSize,multiplier);
}
else {//压缩 有损
if(blocks==1){//压缩,滑块少
windowSize=15;multiplier=1;
}
else{//压缩,滑块多
if(blocks==2){
windowSize=20;multiplier=5;
}
}
HandleData.handleData(data, allTime, 16000, musicTime, window, windowSize,multiplier);
}
}
}
初始化滑块
public void initButtons(){
buttons.add(new ArrayList<EachButton>());//此处需要new初始化不能为null!!!
buttons.add(new ArrayList<EachButton>());
int n=allTime.size();//n个节奏点,n个滑块
for(int i=0;i<n;i++) {
int p=(int)(Math.random()*2);//随机产生轨道数
int x=X[p];//对应轨道的横坐标值
EachButton eachButton = new EachButton(getApplicationContext(),
buttonWidth,buttonHeight,
x, startY, x, endY+microChange, allTime.get(i)-i*2, duration,p);//allTime.get(i)-i*2,处理一个button的时间大概为2ms,调整微小误差
eachButton.start(buttonConstraintLayout);//启动每个滑块的的滑动动画(已延迟,误差时间就是每次生成滑块的那2ms)
buttons.get(p).add(eachButton);//将此滑块加入到该轨道的滑块队列中
}
wholeScore=bestScore*n;//设置原本总的分为全部华块数*best的分值
}
初始化底部按钮方法
public void initBottom(){
bottoms[0]=findViewById(R.id.bottom1);
bottoms[1]=findViewById(R.id.bottom2);
bottoms[0].setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {//点击时如果队列里还有滑块,则去调用initToasts(0),0为轨道数,判断点击结果是good/best,触发toast的展示效果并累计得分
if(buttons.get(0).size()!=0)
initToasts(0);
}
});
bottoms[1].setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if(buttons.get(1).size()!=0)
initToasts(1);
}
});
}
void initToasts(int i){
ObjectAnimator.ofArgb(bottoms[i], "backgroundColor",
Color.parseColor("#F3E77C"),
Color.parseColor("#F8E097"))
.setDuration(500)
.start();//点击时滑块变色的渐变效果,给用户反馈他点击过了
EachButton button=null;
try {
if (buttons.get(i).size() > 0)
button = buttons.get(i).get(0);//得到该轨道最近的一个滑块(index=0,其他的已被移除),这里用新的EachButton赋值,不是直接用队列里的eachButton是为了避免程序计算着滑块又动着
}
catch(Exception e){
e.printStackTrace();
}
toast.cancel();//让上一个toast取消,不然新的toast覆盖不上去
View toastView =LayoutInflater.from(context).inflate(R.layout.toast, null);//得到toast样式
TextView textView=toastView.findViewById(R.id.textView);//得到toast样式里的text文本视图
//最近滑块顶部与按钮顶部的距离
int d=(int)(marginTop-button.getY());//不能用bottoms[i].getY()而用已定的marginTop,底部按钮在linearLayout里面,得不到Y
//good效果,大于滑块一半(三分之一)长度但又触碰到了
if((d>-buttonHeight-microChange&&d<-buttonHeight/3)||(d<bottomHeight+microChange&&d>bottomHeight-buttonHeight/3)) {
goodTimes+=1;//good次数加一
resultScore+=goodScore;//加分
scoreView.setText("Score:"+resultScore);//右上角得分视图更新分数
textView.setText("good");//toast内容更新
buttonConstraintLayout.removeView(button);//从视图中移出滑块
buttons.get(i).remove(0);//从此轨道的滑块数组中移出滑块,(因为每次点击事件取得是第一个滑块)
button.state=false;//滑块状态改为不在视图中
button.animator.cancel();//这里要取消动画,不然虽然视图里的button消失了,但button对象还在,动画还会继续执行
ObjectAnimator.ofArgb(textView, "textColor",
Color.parseColor("#ffffffff"),
Color.parseColor("#00ffffff"))
.setDuration(1000)
.start();//good的toast动画,下降消失效果,绿色
}
else {
//best效果,小于滑块一半长度
if(d<=bottomHeight-buttonHeight/3&&d>=-buttonHeight/3){
bestTimes+=1;
resultScore+=bestScore;
scoreView.setText("Score:"+resultScore);
textView.setText("best");
buttonConstraintLayout.removeView(button);
buttons.get(i).remove(0);
button.state=false;
button.animator.cancel();
ObjectAnimator.ofArgb(textView, "textColor",
Color.parseColor("#ffD9D919"),
Color.parseColor("#00D9D919"))
.setDuration(1000)
.start();//best的toast动画,下降消失效果,金色
}
else {
textView.setText("");//如果不是good也不是best则不显示也不移出,因为会在EachButton的animator的end方法里面显示miss状态
//我也忘记了为什么把miss扔到EachButton类里面了,好像是放这里会出错。。。
}
}
toast=new Toast(context);
toast.setView(toastView);//更新toast视图
toast.setDuration(Toast.LENGTH_SHORT);
toastView.animate().translationY(50)
.setDuration(1000);
toast.show();//xml里的toast只是个更新数据的媒介,真正在活动里显示的是活动中new的这个toast变量
//如果两个轨道的滑块队列都为空,则跳转到成绩结算页面
if(buttons.get(0).size()+buttons.get(1).size()==0){
final Intent iintent=new Intent(GameActivity.this,ResultActivity.class);
iintent.putExtra("wholeScore",wholeScore);
iintent.putExtra("resultScore",resultScore);
iintent.putExtra("bestTimes",bestTimes);
iintent.putExtra("goodTimes",goodTimes);
iintent.putExtra("missTimes",missTimes);
TimerTask task = new TimerTask(){
public void run(){
startActivity(iintent);//跳转到成绩结算页面
}
};
Timer timer = new Timer();
timer.schedule(task, 2000);//经过两秒再跳转,避免结束的过于匆忙的误差,跳转后就启动了onDestroy()关闭播放器
}
}