Android TV 开发之 TV视频播放器,附安卓面经

import butterknife.OnClick;

public class MainActivity extends AppCompatActivity {

@BindView(R.id.tv_test)

TextView tvTest;

@BindView(R.id.btn_test)

Button btnTest;

@Override

protected void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

setContentView(R.layout.activity_main);

ButterKnife.bind(this);

}

@OnClick(R.id.btn_test)

public void onViewClicked() {

//Toast 提示

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

}

}

这个时候你应该迫不及待的想要运行一下了吧,我们还有一步就是主题的设置

打开values下面的styles.xml文件

在这里插入图片描述

我们不用它这个主题,重新创建一个

styles.xml的完整代码如下:

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

然后你会发现少几个颜色,这时候我们在values文件夹下面创建一个colors.xml的文件

colors.xml代码如下:

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

#008577

#00574B

#D81B60

#1EBADE

然后就要使用我们新创建的主题了,打开AndroidManifest.xml文件

在这里插入图片描述

修改,AppTheme,改成AppTheme2,然后运行项目.运行效果如下图

在这里插入图片描述

这个时候你没有想过,我怎么点击这个按钮呢?电视机都是用遥控器的啊,遥控器又怎么操作呢?

这些问题一定在你的脑海里面环绕着,我们注意到,电视机使用遥控器,而我们的手机使用手指触摸点击,这个不能混为一谈,所以电视上需要用到焦点电视上都是通过控件获取焦点来实现点击效果的,我们在布局文件的button中写入

android:focusable=“true”

意思就是可以获取到焦点,为false则不可获取焦点,

在代码里

btnTest.setFocusable(true);

为false则不可获取焦点。

在已知控件ID的情况下我们可以设置上下左右的移动控件,

android:nextFocusUp=“@id/tv_test”

android:nextFocusDown=“@id/tv_test”

android:nextFocusLeft=“@id/tv_test”

android:nextFocusRight=“@id/tv_test”

代码中:

btnTest.setNextFocusUpId(R.id.tv_test);

btnTest.setNextFocusDownId(R.id.tv_test);

btnTest.setNextFocusLeftId(R.id.tv_test);

btnTest.setNextFocusRightId(R.id.tv_test);

了解这个之后,我们还得知道遥控器的按键监听,毕竟是用遥控器来操作的啊,按键监听代码如下:

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—>”);

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);

}

如果你要监听Home键的话,就需要通过广播来,

在MainActivity中创建一个class

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键触发”);

}

}

}

}

在onCreate()方法中注册广播,只要调用initReceiver()方法即可

public final String SYSTEM_DIALOG_REASON_KEY = “reason”;

public final String SYSTEM_DIALOG_REASON_HOME_KEY = “homekey”;

private HomeReceiver homeReceiver;

/**

  • 注册广播

*/

private void initReceiver() {

homeReceiver = new HomeReceiver();

IntentFilter filter = new IntentFilter(Intent.ACTION_CLOSE_SYSTEM_DIALOGS);

registerReceiver(homeReceiver, filter);

}

页面销毁时,注销掉广播

@Override

protected void onDestroy() {

super.onDestroy();

if(homeReceiver!=null){

unregisterReceiver(homeReceiver);

}

}

这段代码我也是从网上找的,

然后我们在确定键的下面弹出这个Toast

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;

运行效果如下:

在这里插入图片描述

MainActivity.java完整代码如下:

package com.llw.androidtvdemo;

import android.content.BroadcastReceiver;

import android.content.Context;

import android.content.Intent;

import android.content.IntentFilter;

import android.os.Bundle;

import android.util.Log;

import android.view.KeyEvent;

import android.widget.Button;

import android.widget.TextView;

import android.widget.Toast;

import androidx.appcompat.app.AppCompatActivity;

import butterknife.BindView;

import butterknife.ButterKnife;

import butterknife.OnClick;

public class MainActivity extends AppCompatActivity {

@BindView(R.id.tv_test)

TextView tvTest;

@BindView(R.id.btn_test)

Button btnTest;

public final String SYSTEM_DIALOG_REASON_KEY = “reason”;

public final String SYSTEM_DIALOG_REASON_HOME_KEY = “homekey”;

private HomeReceiver homeReceiver;

/**

  • 注册广播

*/

private void initReceiver() {

homeReceiver = new HomeReceiver();

IntentFilter filter = new IntentFilter(Intent.ACTION_CLOSE_SYSTEM_DIALOGS);

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;

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

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

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

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

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

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

面试复习笔记:

这份资料我从春招开始,就会将各博客、论坛。网站上等优质的Android开发中高级面试题收集起来,然后全网寻找最优的解答方案。每一道面试题都是百分百的大厂面经真题+最优解答。包知识脉络 + 诸多细节。
节省大家在网上搜索资料的时间来学习,也可以分享给身边好友一起学习。

《960页Android开发笔记》

《1307页Android开发面试宝典》

包含了腾讯、百度、小米、阿里、乐视、美团、58、猎豹、360、新浪、搜狐等一线互联网公司面试被问到的题目。熟悉本文中列出的知识点会大大增加通过前两轮技术面试的几率。

《507页Android开发相关源码解析》

只要是程序员,不管是Java还是Android,如果不去阅读源码,只看API文档,那就只是停留于皮毛,这对我们知识体系的建立和完备以及实战技术的提升都是不利的。

真正最能锻炼能力的便是直接去阅读源码,不仅限于阅读各大系统源码,还包括各种优秀的开源库。

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

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

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

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

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

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

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

面试复习笔记:

这份资料我从春招开始,就会将各博客、论坛。网站上等优质的Android开发中高级面试题收集起来,然后全网寻找最优的解答方案。每一道面试题都是百分百的大厂面经真题+最优解答。包知识脉络 + 诸多细节。
节省大家在网上搜索资料的时间来学习,也可以分享给身边好友一起学习。

《960页Android开发笔记》

[外链图片转存中…(img-aLR7oXKS-1712768768695)]

《1307页Android开发面试宝典》

包含了腾讯、百度、小米、阿里、乐视、美团、58、猎豹、360、新浪、搜狐等一线互联网公司面试被问到的题目。熟悉本文中列出的知识点会大大增加通过前两轮技术面试的几率。

[外链图片转存中…(img-1teBSVz5-1712768768695)]

《507页Android开发相关源码解析》

只要是程序员,不管是Java还是Android,如果不去阅读源码,只看API文档,那就只是停留于皮毛,这对我们知识体系的建立和完备以及实战技术的提升都是不利的。

真正最能锻炼能力的便是直接去阅读源码,不仅限于阅读各大系统源码,还包括各种优秀的开源库。

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

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值