Android Studio 开发实践——简易版音游APP(五)

游戏界面实现

 

 

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()关闭播放器
        }
    }

 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值