需求:
Android端写一个界面,作为TCP服务端,接受客户端发来的图片以及一些信息,显示在界面上。再次打开APP的时候保证上一次图片存在。
思路:
1 编写一个TCP服务端,继承runnable接口的方式去实现,然后写一个接口回调监听TCP接受的数据。
2 主界面监听TCP服务的接口,背景图是一个ImgView,加载使用Bitmap
3 保存图片以及本地数据:文字类的使用sp存储,图片保存在SDCard下,使用File类操作
主要涉及的知识点:
1 TCP
2 线程的创建方法以及优缺点
3 Bitmap的使用
4 Android中操作SD卡
实现以及总结
一 TCP服务
package com.example.namebrand.network;import android.util.Log;import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;/*** 2023/05/13*/
public class TCPServer implements Runnable {private static final String TAG = "TCPServer";private String chaSet = "UTF-8";private int port;private boolean isListen = true;public TCPServer(int port) {this.port = port;}byte[] imgBytes;@Overridepublic void run() {try {ServerSocket serverSocket = new ServerSocket(port);Log.d(TAG, "run:等待客户端连接... ");//serverSocket.setSoTimeout(2000);if (isListen) {Socket socket = serverSocket.accept();Log.d(TAG, "run: 客户端已连接");if (socket != null) {acceptData(socket);}}} catch (IOException e) {e.printStackTrace();}}private void acceptData(Socket socket) {InputStream is;OutputStream os;int postX = 0;int postY = 0;int size = 0;int reserveOne = 0;int reserveTwo = 0;String reserve = "";int color = 0;long pngLen = 0L;int i = 0;byte[] bytesBuffer = null;int bufferPos = 0;int rcvLen = 0;try {is = socket.getInputStream();os = socket.getOutputStream();byte[] bytes = new byte[1024 * 4];while (!socket.isClosed() && !socket.isInputShutdown()) {while ((rcvLen = is.read(bytes)) != -1) {if (rcvLen > 0) {if (i == 0) {byte[] content = new byte[bytes.length];System.arraycopy(bytes, 0, content, 0, bytes.length);String res = new String(content, chaSet);String trim = res.trim(); //打印的时候去掉多余部分Log.d(TAG, "accept: len: " + rcvLen + " content:" + trim);postX = getInt(bytes, 0, 4);postY = getInt(bytes, 4, 4);size = getInt(bytes, 8, 4);reserveOne = getInt(bytes, 12, 4);reserveTwo = getInt(bytes, 16, 4);reserve = getString(bytes, 20, 56);color = getInt(bytes, 76, 4);pngLen = getInt(bytes, 80, 4);if (pngLen > 0) {bytesBuffer = new byte[1024 * 1024 * 5];}i++;}//再无图片数据if (pngLen <= 0) {if (onReceiveListener != null) {onReceiveListener.receive(postX, postY, size, reserveOne, reserveTwo, reserve, color, pngLen,imgBytes);}i = 0;} else {System.arraycopy(bytes, 0, bytesBuffer, bufferPos, rcvLen);bufferPos += rcvLen;Log.d(TAG, "accept: bufferPos:" + bufferPos + "pngLen:" + pngLen + "rvcLen:" + rcvLen);if (bufferPos >= pngLen + 84) {imgBytes = new byte[bufferPos - 84];System.arraycopy(bytesBuffer, 84, imgBytes, 0, bufferPos - 84);if (onReceiveListener != null) {onReceiveListener.receive(postX, postY, size, reserveOne, reserveTwo, reserve, color, pngLen,imgBytes);}bufferPos = 0;pngLen = 0;i = 0;}}}}}socket.close();Log.d(TAG, "accept: 断开连接");} catch (IOException e) {e.printStackTrace();}}//byte转intpublic int getInt(byte[] srcBytes, int srcPos, int length) {byte[] bytes = new byte[length];System.arraycopy(srcBytes, srcPos, bytes, 0, length);int anInt = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN).getInt();return anInt;}//byte转Stringpublic String getString(byte[] srcBytes, int srcPos, int length) {byte[] bytes = new byte[length];System.arraycopy(srcBytes, srcPos, bytes, 0, length);String str = "解析错误";try {str = new String(bytes, 0, length, chaSet);} catch (UnsupportedEncodingException e) {e.printStackTrace();}return str.trim();}private onReceiveListener onReceiveListener;public interface onReceiveListener {void receive(int nPostX, int nPostY, int nSize, int nReserveOne, int nReserveTwo, String reserve, int color, long pngLen,byte[] bytes);}public void setOnReceiveListener(onReceiveListener onReceiveListener) {this.onReceiveListener = onReceiveListener;}
}
这里使用的是Runnable接口来进行创建线程,主要逻辑操作都封装在run方法中,创建一个socket,然后接收数据,由于提前定义好了数据结构,然后我在accpetData中取出对应长度的字节数组,并且根据数据类型转换成Int或者String,由于最后传入的时图片的长度,那么除了前面的84字节,后面的都是图片的内容,然后就把后面接收到的所有字节拼接成一个新的字节数组,这就是图片。
然后就是byte转int和byte转String的方法。
接下来就是定义接口和回调,在上面接收数据的时候,如果再没有图片字节数组,就代表已经传递完毕,调用receive方法将已经接收的数据回调到页面。
二 主界面接收数据以及更新UI
在程序启动后,开启进行接收数据的监听:
@Overrideprotected void onResume() {super.onResume();tcpServer.setOnReceiveListener(new TCPServer.onReceiveListener() {@Overridepublic void receive(int nPostX, int nPostY, int nSize, int nReserveOne, int nReserveTwo, String reserve, int color, long pngLen, byte[] bytes) {Log.d(TAG, "receive\n postX:" + nPostX + "\npostY:" + nPostY + "\nSize:" + nSize + "\nnReserveOne:"+ nReserveOne + "\nnReserveTwo:" + nReserveTwo + "\nreserve:" + reserve + "\ncolor:" + color + "\npngLen:" + pngLen + "\nbytes:" + bytes);//子线程中获取收到的信息 用handler发送给主线程BrandBean brandBean = new BrandBean(nPostX, nPostY, nSize, nReserveOne, nReserveTwo, reserve, color, bytes);Message message = Message.obtain();message.what = 0;Bundle bundle = new Bundle();bundle.putParcelable("brand", brandBean);message.setData(bundle);handler.sendMessage(message);}});}
接收到的数据通过handler发送给主线程,更新UI操作
//主线程用handler接收数据 更新UI(背景+时间)final Handler handler = new Handler() {@Overridepublic void handleMessage(@NonNull Message msg) {super.handleMessage(msg);switch (msg.what) {case 0:Bundle bundle = msg.getData();BrandBean brand = bundle.getParcelable("brand");//更新UIUpdateBg(brand.getTimeX(), brand.getTimeY(), brand.getTimeSize(), brand.getReceiveOne(), brand.getReceiveTwo(), brand.getReceive(), brand.getTimeColor(), brand.getBgImg());}}};
接下来是字节数组转图片,我用的是Bitmap类来进行操作。来介绍下Bitmap,在 Android 中,Bitmap是用于表示图像的类,提供了各种方法和功能来加载、创建、操作和显示图像。下面是对 Bitmap的使用进行详细解释:
加载图像:
- 通过资源加载:使用
BitmapFactory
类的decodeResource()
方法从资源中加载图像。- 通过文件加载:使用
BitmapFactory
类的decodeFile()
方法从文件中加载图像。- 通过字节数组加载:使用
BitmapFactory
类的decodeByteArray()
方法从字节数组中加载图像。- 通过流加载:使用
BitmapFactory
类的decodeStream()
方法从输入流中加载图像。创建图像:
- 使用
Bitmap.createBitmap()
方法创建空白的Bitmap
对象。- 使用
Bitmap.createScaledBitmap()
方法创建按比例缩放的Bitmap
对象。- 使用
Bitmap.createBitmap(int width, int height, Bitmap.Config config)
方法创建指定尺寸和配置的Bitmap
对象。图像操作和处理:
- 裁剪图像:使用
Bitmap.createBitmap()
方法创建裁剪后的新Bitmap
对象。- 缩放图像:使用
Bitmap.createScaledBitmap()
方法创建缩放后的新Bitmap
对象。- 旋转图像:使用
Matrix
类的postRotate()
方法旋转图像,并使用Bitmap.createBitmap()
方法创建旋转后的新Bitmap
对象。- 应用滤镜效果:使用
ColorMatrix
和ColorMatrixColorFilter
类来调整图像颜色和应用滤镜效果。- 修改像素值:使用
setPixel()
和getPixel()
方法直接修改或获取图像的像素值。图像存储和读取:
- 保存图像:使用
compress()
方法将Bitmap
对象保存为指定格式的图像文件。- 读取图像:使用
decodeFile()
方法从文件中读取图像数据为Bitmap
对象。图像显示:
- 使用
ImageView
组件:将Bitmap
对象设置给ImageView
组件,通过setImageBitmap()
方法显示图像。- 使用
Canvas
绘制:使用Canvas
类的drawBitmap()
方法在Canvas
上绘制图像。内存管理和优化:
- 及时回收:通过调用
Bitmap.recycle()
方法及时回收不再需要的Bitmap
对象,释放内存资源。- 优化加载:通过设置
BitmapFactory.Options
对象的inSampleSize
字段来减小图像的尺寸,降低内存占用。- 图像缓存:使用图像缓存库(如 LruCache、DiskLruCache)进行图像的内存和
三 操作文件类 保存图片
操作SD卡的相关代码如下:
/*** 获取SD卡下程序的缓存路径*/public static File getCacheDir(Context context, String name) {File cacheDir;if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) {cacheDir = context.getExternalCacheDir(); // 外部存储 需要手动清理 /sdcard/Android/data/<package_name>/cache/} else {cacheDir = context.getCacheDir(); // 应用内内存 /data/data/<package_name>/cache/}if (cacheDir != null) {if (name != null) {File cacheFile = new File(cacheDir.getAbsolutePath() + "/" + name);//当前cache所在的路径Log.d(TAG, "getCacheDir:33 "+cacheFile);if (createDir(cacheFile)) {cacheDir = cacheFile;Log.d(TAG, "getCacheDir: "+cacheDir);} else {cacheDir = null;}}} else {return null;}return cacheDir;}/*** 创建指定文件夹** @param cacheDir* @return*/private static boolean createDir(File cacheDir) {if (cacheDir == null) {return false;}if (cacheDir.exists() && cacheDir.isDirectory()) {return true;} else {return cacheDir.mkdir();}}
操作文件类写好了,bitmap进行图片的存储和读取也知道对应的方法了,下面就是进行文件夹下的图片的存储和读取:
/*** 向文件夹写入bitmap*/public static boolean writeBitmap(File path, Bitmap bitmap) {FileOutputStream fs = null;Log.d(TAG, "writeBitmap: "+path);try {fs = new FileOutputStream(path.getAbsolutePath());bitmap.compress(Bitmap.CompressFormat.PNG, 100, fs); //将bitmap压缩成PNG形式写入文件夹fs.flush();//刷新输出流 确保缓冲区的数据及时写入 不会丢失return true;} catch (FileNotFoundException e) {e.printStackTrace();} catch (IOException e) {e.printStackTrace();} finally {if (fs != null) {try {fs.close();} catch (IOException e) {e.printStackTrace();}}}return false;}/*** 从指定文件夹下读取bitmap*/public static Bitmap readBitmap(File file) {if (!file.exists()) {return null;} else {BitmapFactory.Options options = new BitmapFactory.Options();options.inPreferredConfig = Bitmap.Config.ARGB_8888;Bitmap bitmap = BitmapFactory.decodeFile(file.getAbsolutePath());return bitmap;}}
在主线程的handler中接收数据进行更新UI的地方调用该方法即可,注意UI更新要在主线程中进行
new Thread(new Runnable() {@Overridepublic void run() {Bitmap fileBitmap = getFileBitmap(IMAGE_BG);runOnUiThread(new Runnable() {@Overridepublic void run() {imgBg.setImageBitmap(fileBitmap);}});}}).start();