android設(shè)備間實(shí)現(xiàn)無線投屏的示例代碼
前言
Android提供了MediaProjection來實(shí)現(xiàn)錄屏,通過MediaProjection可以獲取當(dāng)前屏幕的視頻流,而視頻流需要通過編解碼來壓縮進(jìn)行傳輸,通過MediaCodec可實(shí)現(xiàn)視頻的編碼和解碼。視頻流的推送和接收可通過Socket或WebSocket來實(shí)現(xiàn),服務(wù)端推送編碼的視頻流,客戶端接收視頻流并進(jìn)行解碼,然后渲染在SurfaceView上即可顯示服務(wù)端的畫面。
投屏服務(wù)端的實(shí)現(xiàn)
服務(wù)端主入口負(fù)責(zé)請求錄屏和啟動錄屏服務(wù),服務(wù)端主入口的實(shí)現(xiàn)如下:
package com.example.screenprojection;
import android.content.Intent;
import android.media.projection.MediaProjectionManager;
import android.os.Build;
import android.os.Bundle;
import android.view.View;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
public class MainActivity extends AppCompatActivity {
private static final int PROJECTION_REQUEST_CODE = 1;
private MediaProjectionManager mediaProjectionManager;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
init();
}
private void init() {
mediaProjectionManager = (MediaProjectionManager) getSystemService(MEDIA_PROJECTION_SERVICE);
}
public void onClick(View view) {
if (view.getId() == R.id.btn_start) {
startProjection();
}
}
// 請求開始錄屏
private void startProjection() {
Intent intent = mediaProjectionManager.createScreenCaptureIntent();
startActivityForResult(intent, PROJECTION_REQUEST_CODE);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (resultCode != RESULT_OK) {
return;
}
if (requestCode == PROJECTION_REQUEST_CODE) {
Intent service = new Intent(this, ScreenService.class);
service.putExtra("code", resultCode);
service.putExtra("data", data);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
startForegroundService(service);
} else {
startService(service);
}
}
}
@Override
protected void onDestroy() {
super.onDestroy();
}
}Android8.0后錄屏需要開啟前臺服務(wù)通知,錄屏服務(wù)開啟后同時(shí)啟動WebSocketServer服務(wù)端,前臺服務(wù)代碼如下:
package com.example.screenprojection;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Intent;
import android.graphics.BitmapFactory;
import android.media.projection.MediaProjection;
import android.media.projection.MediaProjectionManager;
import android.os.Build;
import android.os.IBinder;
public class ScreenService extends Service {
private MediaProjectionManager mMediaProjectionManager;
private SocketManager mSocketManager;
@Override
public void onCreate() {
super.onCreate();
mMediaProjectionManager = (MediaProjectionManager) getSystemService(MEDIA_PROJECTION_SERVICE);
createNotificationChannel();
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
int resultCode = intent.getIntExtra("code", -1);
Intent resultData = intent.getParcelableExtra("data");
startProject(resultCode, resultData);
return super.onStartCommand(intent, flags, startId);
}
// 錄屏開始后進(jìn)行編碼推流
private void startProject(int resultCode, Intent data) {
MediaProjection mediaProjection = mMediaProjectionManager.getMediaProjection(resultCode, data);
if (mediaProjection == null) {
return;
}
// 初始化服務(wù)器端
mSocketManager = new SocketManager();
mSocketManager.start(mediaProjection);
}
private void createNotificationChannel() {
Notification.Builder builder = new Notification.Builder(this.getApplicationContext());
Intent nfIntent = new Intent(this, MainActivity.class);
builder
.setContentIntent(PendingIntent.getActivity(this, 0, nfIntent, 0))
.setLargeIcon(
BitmapFactory.decodeResource(
this.getResources(), R.mipmap.ic_launcher)) // 設(shè)置下拉列表中的圖標(biāo)(大圖標(biāo))
.setSmallIcon(R.mipmap.ic_launcher) // 設(shè)置狀態(tài)欄內(nèi)的小圖標(biāo)
.setContentText("is running......") // 設(shè)置上下文內(nèi)容
.setWhen(System.currentTimeMillis()); // 設(shè)置該通知發(fā)生的時(shí)間
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
builder.setChannelId("notification_id");
// 前臺服務(wù)notification適配
NotificationManager notificationManager =
(NotificationManager) getSystemService(NOTIFICATION_SERVICE);
NotificationChannel channel =
new NotificationChannel(
"notification_id", "notification_name", NotificationManager.IMPORTANCE_LOW);
notificationManager.createNotificationChannel(channel);
}
Notification notification = builder.build();
notification.defaults = Notification.DEFAULT_SOUND; // 設(shè)置為默認(rèn)通知音
startForeground(110, notification);
}
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public void onDestroy() {
mSocketManager.close();
super.onDestroy();
}
}SocketManager為WebSocketServer的控制類,并同時(shí)將錄屏對象MediaProjection與視頻編碼器ScreenEncoder綁定。
package com.example.screenprojection;
import android.media.projection.MediaProjection;
import java.net.InetSocketAddress;
public class SocketManager {
private static final String TAG = SocketManager.class.getSimpleName();
private static final int SOCKET_PORT = 50000;
private final ScreenSocketServer mScreenSocketServer;
private ScreenEncoder mScreenEncoder;
public SocketManager() {
mScreenSocketServer = new ScreenSocketServer(new InetSocketAddress(SOCKET_PORT));
}
public void start(MediaProjection mediaProjection) {
mScreenSocketServer.start();
mScreenEncoder = new ScreenEncoder(this, mediaProjection);
mScreenEncoder.startEncode();
}
public void close() {
try {
mScreenSocketServer.stop();
mScreenSocketServer.close();
} catch (InterruptedException e) {
e.printStackTrace();
}
if (mScreenEncoder != null) {
mScreenEncoder.stopEncode();
}
}
public void sendData(byte[] bytes) {
mScreenSocketServer.sendData(bytes);
}
}推流服務(wù)端ScreenSocketServer代碼如下:
package com.example.screenprojection;
import android.util.Log;
import org.java_websocket.WebSocket;
import org.java_websocket.handshake.ClientHandshake;
import org.java_websocket.server.WebSocketServer;
import java.net.InetSocketAddress;
public class ScreenSocketServer extends WebSocketServer {
private final String TAG = ScreenSocketServer.class.getSimpleName();
private WebSocket mWebSocket;
public ScreenSocketServer(InetSocketAddress inetSocketAddress) {
super(inetSocketAddress);
}
@Override
public void onOpen(WebSocket conn, ClientHandshake handshake) {
Log.d(TAG, "onOpen");
mWebSocket = conn;
}
@Override
public void onClose(WebSocket conn, int code, String reason, boolean remote) {
Log.d(TAG, "onClose:" + reason);
}
@Override
public void onMessage(WebSocket conn, String message) {}
@Override
public void onError(WebSocket conn, Exception ex) {
Log.d(TAG, "onError:" + ex.toString());
}
@Override
public void onStart() {
Log.d(TAG, "onStart");
}
public void sendData(byte[] bytes) {
if (mWebSocket != null && mWebSocket.isOpen()) {
// 通過WebSocket 發(fā)送數(shù)據(jù)
Log.d(TAG, "sendData:");
mWebSocket.send(bytes);
}
}
public void close() {
mWebSocket.close();
}
}WebSocketServer啟動成功會回調(diào)onStart方法。
@Override
public void onStart() {
Log.d(TAG, "onStart");
}當(dāng)有客戶端連接回調(diào)onOpen方法,通過回調(diào)的WebSocket對象即可向客戶端發(fā)送數(shù)據(jù)。
@Override
public void onOpen(WebSocket conn, ClientHandshake handshake) {
Log.d(TAG, "onOpen");
mWebSocket = conn;
}視頻編碼器ScreenEncoder采用采用H.265編碼(H.265/HEVC),需要注意不同手機(jī)支持的編碼最大分辨率不同。
package com.example.screenprojection;
import android.hardware.display.DisplayManager;
import android.media.MediaCodec;
import android.media.MediaCodecInfo;
import android.media.MediaFormat;
import android.media.projection.MediaProjection;
import android.view.Surface;
import java.io.IOException;
import java.nio.ByteBuffer;
/**
* 采用H.265編碼 H.265/HEVC的編碼架構(gòu)大致上和H.264/AVC的架構(gòu)相似 H.265又稱為HEVC(全稱High Efficiency Video Coding,高效率視頻編碼)
*/
public class ScreenEncoder extends Thread {
//不同手機(jī)支持的編碼最大分辨率不同
private static final int VIDEO_WIDTH = 2160;
private static final int VIDEO_HEIGHT = 3840;
private static final int SCREEN_FRAME_RATE = 20;
private static final int SCREEN_FRAME_INTERVAL = 1;
private static final long SOCKET_TIME_OUT = 10000;
// I幀
private static final int TYPE_FRAME_INTERVAL = 19;
// vps幀
private static final int TYPE_FRAME_VPS = 32;
private final MediaProjection mMediaProjection;
private final SocketManager mSocketManager;
private MediaCodec mMediaCodec;
private boolean mPlaying = true;
// 記錄vps pps sps
private byte[] vps_pps_sps;
public ScreenEncoder(SocketManager socketManager, MediaProjection mediaProjection) {
mSocketManager = socketManager;
mMediaProjection = mediaProjection;
}
public void startEncode() {
MediaFormat mediaFormat =
MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_HEVC, VIDEO_WIDTH, VIDEO_HEIGHT);
mediaFormat.setInteger(
MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
// 比特率(比特/秒)
mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, VIDEO_WIDTH * VIDEO_HEIGHT);
// 幀率
mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, SCREEN_FRAME_RATE);
// I幀的頻率
mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, SCREEN_FRAME_INTERVAL);
try {
mMediaCodec = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_HEVC);
mMediaCodec.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
Surface surface = mMediaCodec.createInputSurface();
mMediaProjection.createVirtualDisplay(
"screen",
VIDEO_WIDTH,
VIDEO_HEIGHT,
1,
DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC,
surface,
null,
null);
} catch (IOException e) {
e.printStackTrace();
}
start();
}
@Override
public void run() {
mMediaCodec.start();
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
while (mPlaying) {
int outPutBufferId = mMediaCodec.dequeueOutputBuffer(bufferInfo, SOCKET_TIME_OUT);
if (outPutBufferId >= 0) {
ByteBuffer byteBuffer = mMediaCodec.getOutputBuffer(outPutBufferId);
encodeData(byteBuffer, bufferInfo);
mMediaCodec.releaseOutputBuffer(outPutBufferId, false);
}
}
}
private void encodeData(ByteBuffer byteBuffer, MediaCodec.BufferInfo bufferInfo) {
int offSet = 4;
if (byteBuffer.get(2) == 0x01) {
offSet = 3;
}
int type = (byteBuffer.get(offSet) & 0x7E) >> 1;
if (type == TYPE_FRAME_VPS) {
vps_pps_sps = new byte[bufferInfo.size];
byteBuffer.get(vps_pps_sps);
} else if (type == TYPE_FRAME_INTERVAL) {
final byte[] bytes = new byte[bufferInfo.size];
byteBuffer.get(bytes);
byte[] newBytes = new byte[vps_pps_sps.length + bytes.length];
System.arraycopy(vps_pps_sps, 0, newBytes, 0, vps_pps_sps.length);
System.arraycopy(bytes, 0, newBytes, vps_pps_sps.length, bytes.length);
mSocketManager.sendData(newBytes);
} else {
byte[] bytes = new byte[bufferInfo.size];
byteBuffer.get(bytes);
mSocketManager.sendData(bytes);
}
}
public void stopEncode() {
mPlaying = false;
if (mMediaCodec != null) {
mMediaCodec.release();
}
if (mMediaProjection != null) {
mMediaProjection.stop();
}
}
}注意點(diǎn):
1.需要依賴WebSocket相關(guān)jar。
implementation “org.java-websocket:Java-WebSocket:1.5.3”
2.AndroidManifest配置網(wǎng)絡(luò)和前臺服務(wù)權(quán)限。
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
3.配置前臺錄屏服務(wù)foregroundServiceType屬性。
<service
android:name=".ScreenService"
android:enabled="true"
android:exported="true"
android:foregroundServiceType="mediaProjection" />投屏客戶端的實(shí)現(xiàn)
客戶端入口負(fù)責(zé)創(chuàng)建SurfaceView并初始化WebSocket客戶端管理器SocketClientManager。
package com.example.screenplayer;
import android.os.Bundle;
import android.view.Surface;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
public class MainActivity extends AppCompatActivity {
private SocketClientManager mSocketClientManager;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
SurfaceView surfaceView = findViewById(R.id.sv_screen);
surfaceView
.getHolder()
.addCallback(
new SurfaceHolder.Callback() {
@Override
public void surfaceCreated(@NonNull SurfaceHolder holder) {
// 連接到服務(wù)端
initSocketManager(holder.getSurface());
}
@Override
public void surfaceChanged(
@NonNull SurfaceHolder holder, int format, int width, int height) {}
@Override
public void surfaceDestroyed(@NonNull SurfaceHolder holder) {}
});
}
private void initSocketManager(Surface surface) {
mSocketClientManager = new SocketClientManager();
mSocketClientManager.start(surface);
}
@Override
protected void onDestroy() {
if (mSocketClientManager != null) {
mSocketClientManager.stop();
}
super.onDestroy();
}
}SocketClientManager將Surface與解碼器綁定,創(chuàng)建WebSocket客戶端并接收服務(wù)端推送的視頻流。其中URI 需要修改為服務(wù)端的IP地址和端口。
package com.example.screenplayer;
import android.view.Surface;
import java.net.URI;
import java.net.URISyntaxException;
public class SocketClientManager implements ScreenSocketClient.SocketCallback {
private static final int SOCKET_PORT = 50000;
private ScreenDecoder mScreenDecoder;
private ScreenSocketClient mSocketClient;
public void start(Surface surface) {
mScreenDecoder = new ScreenDecoder();
mScreenDecoder.startDecode(surface);
try {
// 需要修改為服務(wù)端的IP地址與端口
URI uri = new URI("ws://192.168.1.3:" + SOCKET_PORT);
mSocketClient = new ScreenSocketClient(this, uri);
mSocketClient.connect();
} catch (URISyntaxException e) {
e.printStackTrace();
}
}
public void stop() {
if (mSocketClient != null) {
mSocketClient.close();
}
if (mScreenDecoder != null) {
mScreenDecoder.stopDecode();
}
}
@Override
public void onReceiveData(byte[] data) {
if (mScreenDecoder != null) {
mScreenDecoder.decodeData(data);
}
}
}ScreenSocketClient代碼如下:
package com.example.screenplayer;
import android.util.Log;
import org.java_websocket.client.WebSocketClient;
import org.java_websocket.handshake.ServerHandshake;
import java.net.URI;
import java.nio.ByteBuffer;
public class ScreenSocketClient extends WebSocketClient {
private static final String TAG = ScreenSocketClient.class.getSimpleName();
private final SocketCallback mSocketCallback;
public ScreenSocketClient(SocketCallback socketCallback, URI serverUri) {
super(serverUri);
mSocketCallback = socketCallback;
}
@Override
public void onOpen(ServerHandshake serverHandshake) {
Log.d(TAG, "onOpen");
}
@Override
public void onMessage(String message) {}
@Override
public void onMessage(ByteBuffer bytes) {
byte[] buf = new byte[bytes.remaining()];
bytes.get(buf);
if (mSocketCallback != null) {
mSocketCallback.onReceiveData(buf);
}
}
@Override
public void onClose(int code, String reason, boolean remote) {
Log.d(TAG, "onClose =" + reason);
}
@Override
public void onError(Exception ex) {
Log.d(TAG, "onError =" + ex.toString());
}
public interface SocketCallback {
void onReceiveData(byte[] data);
}
}不同手機(jī)支持的解碼最大分辨率不同,需要設(shè)置正確的分辨率才不會報(bào)錯(cuò)。投屏解碼器ScreenDecoder代碼如下:
package com.example.screenplayer;
import android.media.MediaCodec;
import android.media.MediaFormat;
import android.view.Surface;
import java.io.IOException;
import java.nio.ByteBuffer;
public class ScreenDecoder {
private static final int VIDEO_WIDTH = 2160;
private static final int VIDEO_HEIGHT = 3840;
private static final long DECODE_TIME_OUT = 10000;
private static final int SCREEN_FRAME_RATE = 20;
private static final int SCREEN_FRAME_INTERVAL = 1;
private MediaCodec mMediaCodec;
public ScreenDecoder() {}
public void startDecode(Surface surface) {
try {
// 配置MediaCodec
mMediaCodec = MediaCodec.createDecoderByType(MediaFormat.MIMETYPE_VIDEO_HEVC);
MediaFormat mediaFormat =
MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_HEVC, VIDEO_WIDTH, VIDEO_HEIGHT);
mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, VIDEO_WIDTH * VIDEO_HEIGHT);
mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, SCREEN_FRAME_RATE);
mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, SCREEN_FRAME_INTERVAL);
mMediaCodec.configure(mediaFormat, surface, null, 0);
mMediaCodec.start();
} catch (IOException e) {
e.printStackTrace();
}
}
public void decodeData(byte[] data) {
int index = mMediaCodec.dequeueInputBuffer(DECODE_TIME_OUT);
if (index >= 0) {
ByteBuffer inputBuffer = mMediaCodec.getInputBuffer(index);
inputBuffer.clear();
inputBuffer.put(data, 0, data.length);
mMediaCodec.queueInputBuffer(index, 0, data.length, System.currentTimeMillis(), 0);
}
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
int outputBufferIndex = mMediaCodec.dequeueOutputBuffer(bufferInfo, DECODE_TIME_OUT);
while (outputBufferIndex > 0) {
mMediaCodec.releaseOutputBuffer(outputBufferIndex, true);
outputBufferIndex = mMediaCodec.dequeueOutputBuffer(bufferInfo, 0);
}
}
public void stopDecode() {
if (mMediaCodec != null) {
mMediaCodec.release();
}
}
}注意點(diǎn):
1.需要依賴WebSocket相關(guān)jar。
implementation “org.java-websocket:Java-WebSocket:1.5.3”
2.AndroidManifest配置網(wǎng)絡(luò)權(quán)限。
<uses-permission android:name="android.permission.INTERNET"/>
效果
左手邊為服務(wù)端,右手邊為客戶端,服務(wù)端投屏到客戶端的效果如下:

遇到的錯(cuò)誤
1.WebSocket出現(xiàn)Permission denied的報(bào)錯(cuò):
onerror =java.net.SocketException: socket failed: EACCES (Permission denied)
問題原因:需要網(wǎng)絡(luò)權(quán)限。
<uses-permission android:name=“android.permission.INTERNET”/>
2.WebSocket出現(xiàn)Host unreachable報(bào)錯(cuò):
onerror =java.net.NoRouteToHostException: Host unreachable
問題原因:不在同一局域網(wǎng)內(nèi)或服務(wù)端未正常啟動。
3.WebSocket出現(xiàn)failed to connect報(bào)錯(cuò):
failed to connect to /192.168.1.3 (port 50000) from /:: (port 38262):,不在同一局域網(wǎng)內(nèi)或服務(wù)端未正常啟動
問題原因:不在同一局域網(wǎng)內(nèi)或服務(wù)端未正常啟動。
客戶端連接成功回調(diào)onOpen方法
@Override
public void onOpen(ServerHandshake handshakedata) {
Log.d(TAG,"SocketClient onOpen");
}4.初始化MediaFormat時(shí)報(bào)IllegalStateException異常:
Caused by: java.lang.IllegalArgumentException
at android.media.MediaCodec.native_configure(Native Method)
at android.media.MediaCodec.configure(MediaCodec.java:2023)
at android.media.MediaCodec.configure(MediaCodec.java:1951)
at com.example.screenprojection.ScreenEncoder.startEncode(ScreenEncoder.java:56)
問題原因:不同手機(jī)支持的最大編解碼分辨率不同,可通過adb shell cat /system/etc/media_codecs.xml的adb命令查看手機(jī)支持的編解碼分辨率。未設(shè)置編碼強(qiáng)制要求的一些配置 會拋出 IllegalStateException。
<MediaCodec name="OMX.hisi.video.decoder.hevc.secure" type="video/hevc" >
<Quirk name="needs-flush-on-all-ports" />
<Limit name="size" min="128x128" max="4096x2304" />
<Limit name="alignment" value="2x2" />
<Limit name="block-size" value="16x16" />
<Limit name="block-count" range="64-36896" />
<Limit name="blocks-per-second" range="99-1106880" />
<Limit name="bitrate" range="1-52428800" />
<Feature name="adaptive-playback" />
<Feature name="secure-playback" required="true" />
<Quirk name="requires-allocate-on-input-ports" />
<Quirk name="requires-allocate-on-output-ports" />
<Limit name="concurrent-instances" max="1" />
</MediaCodec>
<!--config suggestion for hevc -->
<!--VT:1280x720@1Mbps@30fps,640x360@450kpbps@30fps,640x360@250kbps@30fps,640x360@170kbps@15fps-->
<!--DestkTop Share: 1280x720@500kbps@3-7fps,720x450@200kbps@3-7fps-->
<!--Wifi Display:1920x1080@8Mbps@30fps 4Kx2k@27M@30fps-->
<!--Recoding:1920x1080@12Mbps@30fps,4k2k@30Mbps@30fps,1920x1080@40M@120fps,1280X720@40M@120fps-->
<MediaCodec name="OMX.hisi.video.encoder.hevc" type="video/hevc" >
<Limit name="size" min="176x144" max="3840x2160" />
<Limit name="alignment" value="4x4" />
<Limit name="block-size" value="64x64" />
<Limit name="blocks-per-second" range="99-244800" />
<Limit name="bitrate" range="200000-70000000" />
<Quirk name="requires-allocate-on-output-ports" />
<Limit name="concurrent-instances" max="3" />
</MediaCodec>源碼下載地址:無線投屏源碼,百度網(wǎng)盤下載地址如下:
鏈接: https://pan.baidu.com/s/1YQ6UHpUuLOMjp7Wf9DmvSw 提取碼: crri
到此這篇關(guān)于android設(shè)備間實(shí)現(xiàn)無線投屏的文章就介紹到這了,更多相關(guān)android無線投屏內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Android開發(fā)之Activity管理工具類完整示例
這篇文章主要介紹了Android開發(fā)之Activity管理工具類,集合完整實(shí)例形式分析了Android操作Activity創(chuàng)建、添加、獲取、移除等相關(guān)操作技巧,需要的朋友可以參考下2018-01-01
Android仿新浪微博發(fā)布微博界面設(shè)計(jì)(5)
這篇文章主要為大家詳細(xì)介紹了Android仿新浪微博發(fā)布微博界面設(shè)計(jì)方案,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-11-11
Android Zipalign工具優(yōu)化Android APK應(yīng)用
本文主要介紹Android Zipalign工具優(yōu)化Android APK應(yīng)用,這里整理了相關(guān)資料及簡單優(yōu)化實(shí)例,有需要的小伙伴可以參考下2016-09-09
Android自定義View實(shí)現(xiàn)LayoutParams的方法詳解
這篇文章主要為大家詳細(xì)介紹了Android自定義View實(shí)現(xiàn)LayoutParams,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2023-02-02
Android編程使用自定義shape實(shí)現(xiàn)shadow陰影效果的方法
這篇文章主要介紹了Android編程使用自定義shape實(shí)現(xiàn)shadow陰影效果的方法,涉及Android中xml文件布局的相關(guān)技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2015-11-11
Flutter實(shí)現(xiàn)底部導(dǎo)航欄
這篇文章主要為大家詳細(xì)介紹了Flutter實(shí)現(xiàn)底部導(dǎo)航欄的相關(guān)資料,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-02-02
Android編程實(shí)現(xiàn)在自定義對話框中獲取EditText中數(shù)據(jù)的方法
這篇文章主要介紹了Android編程實(shí)現(xiàn)在自定義對話框中獲取EditText中數(shù)據(jù)的方法,結(jié)合實(shí)例形式分析了Android對話框數(shù)據(jù)傳遞相關(guān)操作技巧,需要的朋友可以參考下2018-01-01
淺談Android ASM自動埋點(diǎn)方案實(shí)踐
本篇文章主要介紹了淺談Android ASM自動埋點(diǎn)方案實(shí)踐,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2018-01-01

