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é)請求錄屏和啟動(dòng)錄屏服務(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后錄屏需要開啟前臺(tái)服務(wù)通知,錄屏服務(wù)開啟后同時(shí)啟動(dòng)WebSocketServer服務(wù)端,前臺(tái)服務(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"); // 前臺(tái)服務(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啟動(dòng)成功會(huì)回調(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ò)和前臺(tái)服務(wù)權(quán)限。
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
3.配置前臺(tái)錄屏服務(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è)置正確的分辨率才不會(huì)報(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ù)端未正常啟動(dòng)。
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ù)端未正常啟動(dòng)
問題原因:不在同一局域網(wǎng)內(nèi)或服務(wù)端未正常啟動(dòng)。
客戶端連接成功回調(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)制要求的一些配置 會(huì)拋出 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-01Android仿新浪微博發(fā)布微博界面設(shè)計(jì)(5)
這篇文章主要為大家詳細(xì)介紹了Android仿新浪微博發(fā)布微博界面設(shè)計(jì)方案,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-11-11Android Zipalign工具優(yōu)化Android APK應(yīng)用
本文主要介紹Android Zipalign工具優(yōu)化Android APK應(yīng)用,這里整理了相關(guān)資料及簡單優(yōu)化實(shí)例,有需要的小伙伴可以參考下2016-09-09Android自定義View實(shí)現(xiàn)LayoutParams的方法詳解
這篇文章主要為大家詳細(xì)介紹了Android自定義View實(shí)現(xiàn)LayoutParams,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2023-02-02Android編程使用自定義shape實(shí)現(xiàn)shadow陰影效果的方法
這篇文章主要介紹了Android編程使用自定義shape實(shí)現(xiàn)shadow陰影效果的方法,涉及Android中xml文件布局的相關(guān)技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2015-11-11Flutter實(shí)現(xiàn)底部導(dǎo)航欄
這篇文章主要為大家詳細(xì)介紹了Flutter實(shí)現(xiàn)底部導(dǎo)航欄的相關(guān)資料,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-02-02Android編程實(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自動(dòng)埋點(diǎn)方案實(shí)踐
本篇文章主要介紹了淺談Android ASM自動(dòng)埋點(diǎn)方案實(shí)踐,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2018-01-01