Android TV 开发之 TV视频播放器(1),2024年最新算法常见面试题笔试题

registerReceiver(homeReceiver, filter);

}

@Override

protected void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

setContentView(R.layout.activity_main);

ButterKnife.bind(this);

btnTest.setFocusable(true);

initReceiver();

}

@OnClick(R.id.btn_test)

public void onViewClicked() {

//Toast 提示

Toast.makeText(this,tvTest.getText().toString(),Toast.LENGTH_SHORT).show();

}

class HomeReceiver extends BroadcastReceiver {

@Override

public void onReceive(Context context, Intent intent) {

String action = intent.getAction();

if(action.equals(Intent.ACTION_CLOSE_SYSTEM_DIALOGS)){

String reason = intent.getStringExtra(SYSTEM_DIALOG_REASON_KEY);

if(SYSTEM_DIALOG_REASON_HOME_KEY.equals(reason)){

Toast.makeText(MainActivity.this,“home键触发”,Toast.LENGTH_SHORT).show();

Log.d(TAG, “home键触发”);

}

}

}

}

private String TAG = “key”;

@Override

public boolean onKeyDown(int keyCode, KeyEvent event) {

switch (keyCode) {

case KeyEvent.KEYCODE_ENTER: //确定键enter

case KeyEvent.KEYCODE_DPAD_CENTER:

Log.d(TAG, “enter—>”);

Toast.makeText(this,tvTest.getText().toString(),Toast.LENGTH_SHORT).show();

break;

case KeyEvent.KEYCODE_BACK: //返回键

Log.d(TAG,“back—>”);

return true; //这里由于break会退出,所以我们自己要处理掉 不返回上一层

case KeyEvent.KEYCODE_SETTINGS: //设置键

Log.d(TAG, “setting—>”);

break;

case KeyEvent.KEYCODE_DPAD_DOWN: //向下键

/* 实际开发中有时候会触发两次,所以要判断一下按下时触发 ,松开按键时不触发

  • exp:KeyEvent.ACTION_UP

*/

if (event.getAction() == KeyEvent.ACTION_DOWN) {

Log.d(TAG, “down—>”);

}

break;

case KeyEvent.KEYCODE_DPAD_UP: //向上键

Log.d(TAG, “up—>”);

break;

case KeyEvent.KEYCODE_0: //数字键0

Log.d(TAG, “0—>”);

break;

case KeyEvent.KEYCODE_DPAD_LEFT: //向左键

Log.d(TAG, “left—>”);

break;

case KeyEvent.KEYCODE_DPAD_RIGHT: //向右键

Log.d(TAG, “right—>”);

break;

case KeyEvent.KEYCODE_INFO: //info键

Log.d(TAG, “info—>”);

break;

case KeyEvent.KEYCODE_PAGE_DOWN: //向上翻页键

case KeyEvent.KEYCODE_MEDIA_NEXT:

Log.d(TAG, “page down—>”);

break;

case KeyEvent.KEYCODE_PAGE_UP: //向下翻页键

case KeyEvent.KEYCODE_MEDIA_PREVIOUS:

Log.d(TAG, “page up—>”);

break;

case KeyEvent.KEYCODE_VOLUME_UP: //调大声音键

Log.d(TAG, “voice up—>”);

break;

case KeyEvent.KEYCODE_VOLUME_DOWN: //降低声音键

Log.d(TAG, “voice down—>”);

break;

case KeyEvent.KEYCODE_VOLUME_MUTE: //禁用声音

Log.d(TAG, “voice mute—>”);

break;

default:

break;

}

return super.onKeyDown(keyCode, event);

}

@Override

protected void onDestroy() {

super.onDestroy();

if(homeReceiver!=null){

unregisterReceiver(homeReceiver);

}

}

}

然后我们就要想一下编码的过程和逻辑问题了,

1.播放视频的来源 本地 和 网络

2.播放视频的的停止播放、继续播放、重新播放

3.播放视频时的时间和进度计算

4.播放时候按遥控器左右键时,前进 后退

先想清楚这些问题,才能使编码过程中变得有条理

视频来源


本地:

我们可以在valuse文件夹下面创建一个raw文件夹,在里面放一个mp4短视频文件,(PS:至于在真机存储里面放一个视频,你只要播放路径指定这个视频所在地址,然后再加上文件的读写权限,因为我不是这么实现的,所以就不过多赘述了)

网络:

就是通过一个视频地址来播放视频,既然是通过网络来播放的,我们肯定要有联网的权限啊,在AndroidManifest.xml文件中添加联网许可权限

如下所示

布局文件


<?xml version="1.0" encoding="utf-8"?>

<RelativeLayout 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”

android:orientation=“vertical”

android:gravity=“center”

tools:context=“.MainActivity”>

<LinearLayout

android:layout_width=“match_parent”

android:layout_height=“match_parent”

android:orientation=“vertical”>

<com.llw.androidtvdemo.view.MyVideoView

android:id=“@+id/video_view”

android:layout_width=“match_parent”

android:layout_height=“match_parent”

android:layout_gravity=“center” />

<RelativeLayout

android:background=“@drawable/shape_gradual_change”

android:layout_alignParentBottom=“true”

android:layout_width=“match_parent”

android:layout_height=“@dimen/dp_100”>

<LinearLayout

android:gravity=“center_vertical”

android:layout_margin=“@dimen/dp_10”

android:layout_alignParentBottom=“true”

android:orientation=“horizontal”

android:layout_width=“match_parent”

android:layout_height=“wrap_content”>

<TextView

android:id=“@+id/tv_play_time”

android:text=“00:00”

android:textSize=“@dimen/sp_24”

android:textColor=“@color/white”

android:layout_width=“wrap_content”

android:layout_height=“wrap_content”/>

<SeekBar

android:layout_marginLeft=“@dimen/dp_20”

android:layout_marginRight=“@dimen/dp_20”

android:id=“@+id/time_seekBar”

android:layout_width=“0dp”

android:layout_weight=“1”

android:layout_height=“wrap_content”

android:layout_centerInParent=“true”

android:max=“100”

android:maxHeight=“3dp”

android:minHeight=“3dp”

android:progress=“0”

android:progressDrawable=“@drawable/seekbar_style”

android:thumb=“@drawable/thumb” />

<TextView

android:id=“@+id/tv_total_time”

android:text=“00:00”

android:textSize=“@dimen/sp_24”

android:textColor=“@color/white”

android:layout_width=“wrap_content”

android:layout_height=“wrap_content”/>

<RelativeLayout

android:visibility=“gone”

android:id=“@+id/lay_finish_bg”

android:background=“#000”

android:layout_width=“match_parent”

android:layout_height=“match_parent”/>

<ImageButton

android:visibility=“gone”

android:focusable=“true”

android:layout_centerInParent=“true”

android:id=“@+id/btn_play_or_pause”

android:background=“@mipmap/icon_pause”

android:layout_width=“@dimen/dp_100”

android:layout_height=“@dimen/dp_100”/>

<ImageButton

android:visibility=“gone”

android:layout_centerInParent=“true”

android:id=“@+id/btn_restart_play”

android:background=“@mipmap/icon_restart_play”

android:layout_width=“@dimen/dp_100”

android:layout_height=“@dimen/dp_100”/>

注释已经加在布局文件里面了,下面就不过多讲述了,布局文件中的自定义VideoView代码如下:

package com.llw.androidtvdemo.view;

import android.content.Context;

import android.net.Uri;

import android.util.AttributeSet;

import android.widget.VideoView;

import com.llw.androidtvdemo.view.util.SSlUtiles;

import javax.net.ssl.HttpsURLConnection;

/**

  • 自定义VideoView

*/

public class MyVideoView extends VideoView {

public MyVideoView(Context context) {

super(context);

}

public MyVideoView(Context context, AttributeSet attrs) {

super(context, attrs);

}

public MyVideoView(Context context, AttributeSet attrs, int defStyle) {

super(context, attrs, defStyle);

}

@Override

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

//super.onMeasure(widthMeasureSpec, heightMeasureSpec);

int width = getDefaultSize(getWidth(), widthMeasureSpec);

int height = getDefaultSize(getHeight(), heightMeasureSpec);

setMeasuredDimension(width, height);

}

@Override

public void setVideoURI(Uri uri) {

super.setVideoURI(uri);

try {

HttpsURLConnection.setDefaultSSLSocketFactory(SSlUtiles.createSSLSocketFactory());

HttpsURLConnection.setDefaultHostnameVerifier(new SSlUtiles.TrustAllHostnameVerifier());

} catch (Exception e) {

e.printStackTrace();

}

}

}

自定义VideoView中SSlUtils网络证书许可类代码如下:

package com.llw.androidtvdemo.view.util;

import java.security.SecureRandom;

import java.security.cert.CertificateException;

import java.security.cert.X509Certificate;

import javax.net.ssl.HostnameVerifier;

import javax.net.ssl.SSLContext;

import javax.net.ssl.SSLSession;

import javax.net.ssl.SSLSocketFactory;

import javax.net.ssl.TrustManager;

import javax.net.ssl.X509TrustManager;

public class SSlUtils {

/**

  • 默认信任所有的证书

  • xts

*/

public static SSLSocketFactory createSSLSocketFactory() {

SSLSocketFactory sSLSocketFactory = null;

try {

SSLContext sc = SSLContext.getInstance(“TLS”);

sc.init(null, new TrustManager[] { (TrustManager) new TrustAllManager() }, new SecureRandom());

sSLSocketFactory = sc.getSocketFactory();

} catch (Exception e) {

}

return sSLSocketFactory;

}

public static class TrustAllManager implements X509TrustManager {

@Override

public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {

}

@Override

public void checkServerTrusted(X509Certificate[] chain, String authType)

throws CertificateException {

}

@Override

public X509Certificate[] getAcceptedIssuers() {

return new X509Certificate[0];

}

}

public static class TrustAllHostnameVerifier implements HostnameVerifier {

public boolean verify(String hostname, SSLSession session) {

return true;

}

}

}

这个类主要是针对于 VideoView 无法播放此视频 问题,如果你没有这个问题的话,可以在MyVideoView去掉下面这一段代码:

@Override

public void setVideoURI(Uri uri) {

super.setVideoURI(uri);

try {

HttpsURLConnection.setDefaultSSLSocketFactory(SSlUtils.createSSLSocketFactory());

HttpsURLConnection.setDefaultHostnameVerifier(new SSlUtils.TrustAllHostnameVerifier());

} catch (Exception e) {

e.printStackTrace();

}

}

然后来看MainActivity中的代码,通过注解的方式我的控件已经不需要声明和findById了。

首先配置一下我们的VideoVIew

/**

  • 初始化VideoView

*/

private void initVideo() {

//本地视频

// videoView.setVideoURI(Uri.parse(“android.resource://” + getPackageName() + “/raw/test”));

//网络视频

final Uri uri = Uri.parse(“http://gslb.miaopai.com/stream/ed5HCfnhovu3tyIQAiv60Q__.mp4”);

videoView.setVideoURI(uri);

videoView.requestFocus();

videoView.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {

@Override

public void onPrepared(MediaPlayer mp) {

int totalTime = videoView.getDuration();//获取视频的总时长

tvTotalTime.setText(stringForTime(totalTime));//设置视频总时间,stringForTime是写的一个时间装换方法,下面会提到

// 开始线程,更新进度条的刻度

handler.postDelayed(runnable, 0);

timeSeekBar.setMax(videoView.getDuration());

//视频加载完成,准备好播放视频的回调

videoView.start();

}

});

}

上面的初始化中用到了一个线程,线程代码如下:

private Handler handler = new Handler();

private Runnable runnable = new Runnable() {

public void run() {

if (videoView.isPlaying()) {

int current = videoView.getCurrentPosition();//获取播放过程中位置

timeSeekBar.setProgress(current);//设置进度条的位置

tvPlayTime.setText(time(videoView.getCurrentPosition()));//播放过程中的时间

}

handler.postDelayed(runnable, 500);//播放过程中0.5秒执行一次

}

};

然后是onCreate()方法

@Override

protected void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

setContentView(R.layout.activity_main);

ButterKnife.bind(this);//注释然后自动添加的

timeSeekBar.setOnSeekBarChangeListener(onSeekBarChangeListener);//添加进度条的变化监听

initVideo();//初始化VideoView

//videoView播放完成监听

videoView.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {

@Override

public void onCompletion(MediaPlayer mp) {

key = 1;//这是一个全局变量,用于控制遥控单击确定或者ok键时,是暂停继续还是重新播放,1则是重新播放视频

btnRestartPlay.setVisibility(View.VISIBLE);//显示黑色背景,布局文件中注释提到了

layFinishBg.setVisibility(View.VISIBLE);//显示白色重播图标,布局文件中注释提到了

}

});

//videoView播放异常监听,类似于 此视频无法播放 这样的错误提示

videoView.setOnErrorListener(new MediaPlayer.OnErrorListener() {

@Override

public boolean onError(MediaPlayer mp, int what, int extra) {

Toast.makeText(MainActivity.this, “播放出错”, Toast.LENGTH_SHORT).show();

return false;

}

});

}

代码中用到的一个方法:

/**

  • 时间转换方法

  • @param millionSeconds

  • @return

*/

protected String time(long millionSeconds) {

SimpleDateFormat simpleDateFormat = new SimpleDateFormat(“mm:ss”);

Calendar c = Calendar.getInstance();

c.setTimeInMillis(millionSeconds);

return simpleDateFormat.format(c.getTime());

}

控制视频是 播放还是暂停 或者是重播

/**

  • 控制视频是 播放还是暂停 或者是重播

  • @param isPlay

  • @param keys

*/

private void isVideoPlay(boolean isPlay, int keys) {

switch (keys) {

case 0:

if (isPlay) {//暂停

btnPlayOrPause.setBackground(getResources().getDrawable(R.mipmap.icon_player));//更换按钮的图标并显示出来

btnPlayOrPause.setVisibility(View.VISIBLE);

videoView.pause();

} else {//继续播放

btnPlayOrPause.setBackground(getResources().getDrawable(R.mipmap.icon_pause));//更换按钮的图标并显示出来

btnPlayOrPause.setVisibility(View.VISIBLE);

// 开始线程,更新进度条的刻度

handler.postDelayed(runnable, 0);

videoView.start();//继续播放

timeSeekBar.setMax(videoView.getDuration());

timeGone();//当我们选择继续播放之后,就不能让这个图标一直显示下去,但是又不能马上消失,这样很突兀,所以用了延时1.5秒隐藏,比较合理,这个方法后面会贴出来。

}

break;

case 1://重新播放

initVideo();

btnRestartPlay.setVisibility(View.GONE);//白色重播图标隐藏

layFinishBg.setVisibility(View.GONE);//黑色背景隐藏

key = 0;//重新播放之后,我们再将key置为0,这样就不会影响到下一次视频播放过程中的暂停和继续的监听操作了

break;

}

延时1.5秒隐藏

private void timeGone() {

new Handler().postDelayed(new Runnable() {

@Override

public void run() {

btnPlayOrPause.setVisibility(View.INVISIBLE);

}

}, 1500);

}

进度条监听

/**

  • 进度条监听

*/

private SeekBar.OnSeekBarChangeListener onSeekBarChangeListener = new SeekBar.OnSeekBarChangeListener() {

// 当进度条停止修改的时候触发

@Override

public void onStopTrackingTouch(SeekBar seekBar) {

// 取得当前进度条的刻度

int progress = seekBar.getProgress();

if (videoView.isPlaying()) {

// 设置当前播放的位置

videoView.seekTo(progress);

}

}

@Override

public void onStartTrackingTouch(SeekBar seekBar) {

}

@Override

public void onProgressChanged(SeekBar seekBar, int progress,

boolean fromUser) {

}

};

时间转换方法

//将长度转换为时间

StringBuilder mFormatBuilder = new StringBuilder();

Formatter mFormatter = new Formatter(mFormatBuilder, Locale.getDefault());

//整数类型转换为时分秒

private String stringForTime(int timeMs) {

int totalSeconds = timeMs / 1000;

int seconds = totalSeconds % 60;

int minutes = (totalSeconds / 60) % 60;

int hours = totalSeconds / 3600;

mFormatBuilder.setLength(0);

if (hours > 0) {

return mFormatter.format(“%d:%02d:%02d”, hours, minutes, seconds).toString();

} else {

return mFormatter.format(“%02d:%02d”, minutes, seconds).toString();

}

}

遥控器按键监听

private String TAG = “key”;

/**

  • 遥控器按键监听

  • @param keyCode

  • @param event

  • @return

*/

@Override

public boolean onKeyDown(int keyCode, KeyEvent event) {

switch (keyCode) {

case KeyEvent.KEYCODE_ENTER: //确定键enter

case KeyEvent.KEYCODE_DPAD_CENTER:

Log.d(TAG, “enter—>”);

//如果是播放中则暂停、如果是暂停则继续播放

isVideoPlay(videoView.isPlaying(), key);

break;

case KeyEvent.KEYCODE_BACK: //返回键

Log.d(TAG,“back—>”);

return true; //这里由于break会退出,所以我们自己要处理掉 不返回上一层

case KeyEvent.KEYCODE_SETTINGS: //设置键

Log.d(TAG, “setting—>”);

break;

case KeyEvent.KEYCODE_DPAD_DOWN: //向下键

/* 实际开发中有时候会触发两次,所以要判断一下按下时触发 ,松开按键时不触发

  • exp:KeyEvent.ACTION_UP

*/

if (event.getAction() == KeyEvent.ACTION_DOWN) {

Log.d(TAG, “down—>”);

}

break;

case KeyEvent.KEYCODE_DPAD_UP: //向上键

Log.d(TAG, “up—>”);

break;

case KeyEvent.KEYCODE_DPAD_LEFT: //向左键

Log.d(TAG, “left—>”);

if (videoView.getCurrentPosition() > 4) {

videoView.seekTo(videoView.getCurrentPosition() - 5 * 1000);

}

break;

case KeyEvent.KEYCODE_DPAD_RIGHT: //向右键

Log.d(TAG, “right—>”);

videoView.seekTo(videoView.getCurrentPosition() + 5 * 1000);

break;

case KeyEvent.KEYCODE_VOLUME_UP: //调大声音键

Log.d(TAG, “voice up—>”);

break;

case KeyEvent.KEYCODE_VOLUME_DOWN: //降低声音键

Log.d(TAG, “voice down—>”);

break;

case KeyEvent.KEYCODE_VOLUME_MUTE: //禁用声音

Log.d(TAG, “voice mute—>”);

break;

default:

break;

}

return super.onKeyDown(keyCode, event);

}

前进后退的控制都这个遥控器监听里面。

MainActivity的完整代码如下:

package com.llw.androidtvdemo;

import android.media.MediaPlayer;

import android.net.Uri;

import android.os.Bundle;

import android.os.Handler;

import android.util.Log;

import android.view.KeyEvent;

import android.view.View;

import android.widget.ImageButton;

import android.widget.RelativeLayout;

import android.widget.SeekBar;

import android.widget.TextView;

import android.widget.Toast;

import androidx.appcompat.app.AppCompatActivity;

import com.llw.androidtvdemo.view.MyVideoView;

import java.text.SimpleDateFormat;

import java.util.Calendar;

import java.util.Formatter;

import java.util.Locale;

import butterknife.BindView;

import butterknife.ButterKnife;

public class MainActivity extends AppCompatActivity {

@BindView(R.id.video_view)

MyVideoView videoView;

@BindView(R.id.tv_play_time)

TextView tvPlayTime;

@BindView(R.id.time_seekBar)

SeekBar timeSeekBar;

@BindView(R.id.tv_total_time)

TextView tvTotalTime;

@BindView(R.id.lay_finish_bg)

RelativeLayout layFinishBg;

@BindView(R.id.btn_play_or_pause)

ImageButton btnPlayOrPause;

@BindView(R.id.btn_restart_play)

ImageButton btnRestartPlay;

private int key = 0;

private Handler handler = new Handler();

private Runnable runnable = new Runnable() {

public void run() {

if (videoView.isPlaying()) {

int current = videoView.getCurrentPosition();

timeSeekBar.setProgress(current);

tvPlayTime.setText(time(videoView.getCurrentPosition()));

}

handler.postDelayed(runnable, 500);

}

};

@Override

protected void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

setContentView(R.layout.activity_main);

ButterKnife.bind(this);

timeSeekBar.setOnSeekBarChangeListener(onSeekBarChangeListener);

initVideo();

videoView.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {

@Override

public void onCompletion(MediaPlayer mp) {

key = 1;

btnRestartPlay.setVisibility(View.VISIBLE);

layFinishBg.setVisibility(View.VISIBLE);

}

});

videoView.setOnErrorListener(new MediaPlayer.OnErrorListener() {

@Override

public boolean onError(MediaPlayer mp, int what, int extra) {

Toast.makeText(MainActivity.this, “播放出错”, Toast.LENGTH_SHORT).show();

return false;

}

});

}

/**

  • 时间转换方法

  • @param millionSeconds

  • @return

*/

protected String time(long millionSeconds) {

SimpleDateFormat simpleDateFormat = new SimpleDateFormat(“mm:ss”);

Calendar c = Calendar.getInstance();

c.setTimeInMillis(millionSeconds);

return simpleDateFormat.format(c.getTime());

}

/**

  • 初始化VideoView

*/

private void initVideo() {

//本地视频

// videoView.setVideoURI(Uri.parse(“android.resource://” + getPackageName() + “/raw/test”));

//网络视频

final Uri uri = Uri.parse(“http://gslb.miaopai.com/stream/ed5HCfnhovu3tyIQAiv60Q__.mp4”);

videoView.setVideoURI(uri);

videoView.requestFocus();

videoView.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {

@Override

public void onPrepared(MediaPlayer mp) {

int totalTime = videoView.getDuration();//获取视频的总时长

tvTotalTime.setText(stringForTime(totalTime));

// 开始线程,更新进度条的刻度

handler.postDelayed(runnable, 0);

timeSeekBar.setMax(videoView.getDuration());

//视频加载完成,准备好播放视频的回调

videoView.start();

}

});

}

/**

  • 控制视频是 播放还是暂停 或者是重播

  • @param isPlay

  • @param keys

*/

private void isVideoPlay(boolean isPlay, int keys) {

switch (keys) {

case 0:

if (isPlay) {//暂停

btnPlayOrPause.setBackground(getResources().getDrawable(R.mipmap.icon_player));

btnPlayOrPause.setVisibility(View.VISIBLE);

videoView.pause();

} else {//继续播放

btnPlayOrPause.setBackground(getResources().getDrawable(R.mipmap.icon_pause));

btnPlayOrPause.setVisibility(View.VISIBLE);

// 开始线程,更新进度条的刻度

handler.postDelayed(runnable, 0);

videoView.start();

timeSeekBar.setMax(videoView.getDuration());

timeGone();

}

break;

case 1://重新播放

initVideo();

btnRestartPlay.setVisibility(View.GONE);

layFinishBg.setVisibility(View.GONE);

key = 0;

break;

}

}

/**

  • 延时隐藏

*/

private void timeGone() {

new Handler().postDelayed(new Runnable() {

@Override

public void run() {

btnPlayOrPause.setVisibility(View.INVISIBLE);

}

}, 1500);

}

/**

  • 进度条监听

*/

private SeekBar.OnSeekBarChangeListener onSeekBarChangeListener = new SeekBar.OnSeekBarChangeListener() {

// 当进度条停止修改的时候触发

@Override

public void onStopTrackingTouch(SeekBar seekBar) {

// 取得当前进度条的刻度

int progress = seekBar.getProgress();

if (videoView.isPlaying()) {

// 设置当前播放的位置

videoView.seekTo(progress);

}

}

@Override

public void onStartTrackingTouch(SeekBar seekBar) {

}

@Override

public void onProgressChanged(SeekBar seekBar, int progress,

boolean fromUser) {

}

};

//将长度转换为时间

StringBuilder mFormatBuilder = new StringBuilder();

Formatter mFormatter = new Formatter(mFormatBuilder, Locale.getDefault());

private String stringForTime(int timeMs) {

int totalSeconds = timeMs / 1000;

int seconds = totalSeconds % 60;

int minutes = (totalSeconds / 60) % 60;

int hours = totalSeconds / 3600;

mFormatBuilder.setLength(0);

if (hours > 0) {

return mFormatter.format(“%d:%02d:%02d”, hours, minutes, seconds).toString();

} else {

return mFormatter.format(“%02d:%02d”, minutes, seconds).toString();

}

}

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
img
img
img
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新

如果你觉得这些内容对你有帮助,可以添加V获取:vip204888 (备注Android)
img

最后

如果你看到了这里,觉得文章写得不错就给个赞呗?如果你觉得那里值得改进的,请给我留言。一定会认真查询,修正不足。谢谢。

欢迎大家一起交流讨论啊~

一个人可以走的很快,但一群人才能走的更远。不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎扫码加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
img

}

@Override

public void onProgressChanged(SeekBar seekBar, int progress,

boolean fromUser) {

}

};

//将长度转换为时间

StringBuilder mFormatBuilder = new StringBuilder();

Formatter mFormatter = new Formatter(mFormatBuilder, Locale.getDefault());

private String stringForTime(int timeMs) {

int totalSeconds = timeMs / 1000;

int seconds = totalSeconds % 60;

int minutes = (totalSeconds / 60) % 60;

int hours = totalSeconds / 3600;

mFormatBuilder.setLength(0);

if (hours > 0) {

return mFormatter.format(“%d:%02d:%02d”, hours, minutes, seconds).toString();

} else {

return mFormatter.format(“%02d:%02d”, minutes, seconds).toString();

}

}

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
[外链图片转存中…(img-V2HhIaPC-1712768737935)]
[外链图片转存中…(img-bR22c4di-1712768737935)]
[外链图片转存中…(img-jdj4k6E2-1712768737936)]
[外链图片转存中…(img-djOEzttJ-1712768737936)]
[外链图片转存中…(img-nqZBlVEi-1712768737936)]
[外链图片转存中…(img-hUKjqssC-1712768737937)]
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新

如果你觉得这些内容对你有帮助,可以添加V获取:vip204888 (备注Android)
[外链图片转存中…(img-E9ifwrtP-1712768737937)]

最后

如果你看到了这里,觉得文章写得不错就给个赞呗?如果你觉得那里值得改进的,请给我留言。一定会认真查询,修正不足。谢谢。

[外链图片转存中…(img-KiiNdQJR-1712768737937)]

欢迎大家一起交流讨论啊~

一个人可以走的很快,但一群人才能走的更远。不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎扫码加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
[外链图片转存中…(img-nxr6FnwM-1712768737938)]

  • 5
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值