Android瀑布流照片墙完成,体验不法则摆列的美感
来源:新途职业教育学校|发布时间:2016-05-21|浏览量:
传统界面的构造格式老是行列分明、座落有序的,这类构造曾经是视而不见,在不知不觉中巨匠都已对它发作了审美委靡。这个时辰瀑布流构造的呈现,就给人带来了线人一新的感受,这类构造固然看上往貌似毫无纪律,可是却有一种说不下去的美感,致使于呈现出了多量的网站和应用纷纭应用这类新奇的构造来想象界面。
记得我在之前已写过一篇关于若何在Android上完成照片墙功用的文章了,但阿谁时辰是应用的GridView来中止构造的,这类构造格式只合用于“墙”上的每张图片巨细都不异的环境,若是图片的巨细良莠不齐,在GridView中显现就会很是的丢脸。而应用瀑布流的构造格式便能够很好地处置这个标题,是以明天我们也来赶一下潮水,看看若何在Android上完成瀑布流照片墙的功用。
起首仍是讲一下完成道理,瀑布流的构造格式固然看起来似乎摆列的很随意,真实它是有很迷信的摆列规律的。全部界面会依照屏幕的宽度划分红等宽的若干列,由于手机的屏幕不是很大,这里我们就分红三列。每当需求添加一张图片时,会将这张图片的宽度紧缩成和列一样宽,再按照一样的紧缩比例对图片的高度中止紧缩,然后在这三列中找出以后高度最小的一列,将图片添加到这一列中。以后每当需求添加一张新图片时,都往重复下面的支配,就会构成瀑布流样式的照片墙,表示图以下所示。
听我这么说完后,你可以会觉得瀑布流的构造很是俭朴嘛,只需求应用三个LinearLayout等分全部屏幕宽度,然后静态地addView()出来就行了。确切如斯,若是只是为了完胜利用的话,就是这么俭朴。可是别忘了,我们是在手机出息行开拓,若是不时地往LinearLayout里添加图片,法式很快就会OOM。是以我们还需求一个公允的计划来对图片资本中止释放,这里仍然是筹办应用LruCache算法,对这个算法不熟习的伴侣能够先参考Android高效加载大图、多图计划,有用避免法式OOM%20。
上面我们就来起头完成吧,新建一个Android项目,起名叫PhotoWallFallsDemo,并选择4.0的API。
第一个要推敲的标题是,我们到哪儿往搜集这些巨细良莠不齐的图片呢?这里我事前在百度上搜刮了良多张风光图片,而且为了包管它们拜候的不变性,我将这些图片都上传到了我的CSDN相册里,是以只需从这里下载图片便能够了。新建一个Images类,将一切相册中图片的网址都设置配备摆设出来,代码以下所示:
[java]%20view%20plaincopypublic%20class%20Images%20{%20
%20
%20 %20 public%20final%20static%20String[]%20imageUrls%20=%20new%20String[]%20{%20
%20 %20 %20 %20 %20 %20 %20//img.my.csdn.net/uploads/201309/01/1378037235_3453.jpg ,
//img.my.csdn.net/uploads/201309/01/1378037235_7476.jpg ,
//img.my.csdn.net/uploads/201309/01/1378037235_9280.jpg ,
//img.my.csdn.net/uploads/201309/01/1378037234_3539.jpg ,
//img.my.csdn.net/uploads/201309/01/1378037234_6318.jpg ,
//img.my.csdn.net/uploads/201309/01/1378037194_2965.jpg ,
//img.my.csdn.net/uploads/201309/01/1378037193_1687.jpg ,
//img.my.csdn.net/uploads/201309/01/1378037193_1286.jpg ,
//img.my.csdn.net/uploads/201309/01/1378037192_8379.jpg ,
//img.my.csdn.net/uploads/201309/01/1378037178_9374.jpg ,
//img.my.csdn.net/uploads/201309/01/1378037177_1254.jpg ,
//img.my.csdn.net/uploads/201309/01/1378037177_6203.jpg ,
//img.my.csdn.net/uploads/201309/01/1378037152_6352.jpg ,
//img.my.csdn.net/uploads/201309/01/1378037151_9565.jpg ,
//img.my.csdn.net/uploads/201309/01/1378037151_7904.jpg ,
//img.my.csdn.net/uploads/201309/01/1378037148_7104.jpg ,
//img.my.csdn.net/uploads/201309/01/1378037129_8825.jpg ,
//img.my.csdn.net/uploads/201309/01/1378037128_5291.jpg ,
//img.my.csdn.net/uploads/201309/01/1378037128_3531.jpg ,
//img.my.csdn.net/uploads/201309/01/1378037127_1085.jpg ,
//img.my.csdn.net/uploads/201309/01/1378037095_7515.jpg ,
//img.my.csdn.net/uploads/201309/01/1378037094_8001.jpg ,
//img.my.csdn.net/uploads/201309/01/1378037093_7168.jpg ,
//img.my.csdn.net/uploads/201309/01/1378037091_4950.jpg ,
//img.my.csdn.net/uploads/201308/31/1377949643_6410.jpg ,
//img.my.csdn.net/uploads/201308/31/1377949642_6939.jpg ,
//img.my.csdn.net/uploads/201308/31/1377949630_4505.jpg ,
//img.my.csdn.net/uploads/201308/31/1377949630_4593.jpg ,
//img.my.csdn.net/uploads/201308/31/1377949629_7309.jpg ,
//img.my.csdn.net/uploads/201308/31/1377949629_8247.jpg ,
//img.my.csdn.net/uploads/201308/31/1377949615_1986.jpg ,
//img.my.csdn.net/uploads/201308/31/1377949614_8482.jpg ,
//img.my.csdn.net/uploads/201308/31/1377949614_3743.jpg ,
//img.my.csdn.net/uploads/201308/31/1377949614_4199.jpg ,
//img.my.csdn.net/uploads/201308/31/1377949599_3416.jpg ,
//img.my.csdn.net/uploads/201308/31/1377949599_5269.jpg ,
//img.my.csdn.net/uploads/201308/31/1377949598_7858.jpg ,
//img.my.csdn.net/uploads/201308/31/1377949598_9982.jpg ,
//img.my.csdn.net/uploads/201308/31/1377949578_2770.jpg ,
//img.my.csdn.net/uploads/201308/31/1377949578_8744.jpg ,
//img.my.csdn.net/uploads/201308/31/1377949577_5210.jpg ,
//img.my.csdn.net/uploads/201308/31/1377949577_1998.jpg ,
//img.my.csdn.net/uploads/201308/31/1377949482_8813.jpg ,
//img.my.csdn.net/uploads/201308/31/1377949481_6577.jpg ,
//img.my.csdn.net/uploads/201308/31/1377949480_4490.jpg ,
//img.my.csdn.net/uploads/201308/31/1377949455_6792.jpg ,
//img.my.csdn.net/uploads/201308/31/1377949455_6345.jpg ,
//img.my.csdn.net/uploads/201308/31/1377949442_4553.jpg ,
//img.my.csdn.net/uploads/201308/31/1377949441_8987.jpg ,
//img.my.csdn.net/uploads/201308/31/1377949441_5454.jpg ,
//img.my.csdn.net/uploads/201308/31/1377949454_6367.jpg ,
//img.my.csdn.net/uploads/201308/31/1377949442_4562.jpg };
}
然后新建一个ImageLoader类,用于便利对图片中止办理,代码以下所示:[java] view plaincopypublic class ImageLoader {
/**
* 图片缓存手艺的焦点类,用于缓存一切下载好的图片,在法式内存抵达设定值时会将最少比来应用的图片移除失落。
*/
private static LruCache String, Bitmap mMemoryCache;
/**
* ImageLoader的实例。
*/
private static ImageLoader mImageLoader;
private ImageLoader() {
// 取得应用法式最大可用内存
int maxMemory = (int) Runtime.getRuntime().maxMemory();
int cacheSize = maxMemory / 8;
// 设置图片缓存巨细为法式最大可用内存的1/8
mMemoryCache = new LruCache String, Bitmap (cacheSize) {
@Override
protected int sizeOf(String key, Bitmap bitmap) {
return bitmap.getByteCount();
}
};
}
/**
* 取得ImageLoader的实例。
*
* @return ImageLoader的实例。
*/
public static ImageLoader getInstance() {
if (mImageLoader == null) {
mImageLoader = new ImageLoader();
}
return mImageLoader;
}
/**
* 将一张图片存储到LruCache中。
*
* @param key
* LruCache的键,这里传进图片的URL地址。
* @param bitmap
* LruCache的键,这里传进从搜集上下载的Bitmap对象。
*/
public void addBitmapToMemoryCache(String key, Bitmap bitmap) {
if (getBitmapFromMemoryCache(key) == null) {
mMemoryCache.put(key, bitmap);
}
}
/**
* 从LruCache中取得一张图片,若是不存在就前往null。
*
* @param key
* LruCache的键,这里传进图片的URL地址。
* @return 对应传进键的Bitmap对象,或null。
*/
public Bitmap getBitmapFromMemoryCache(String key) {
return mMemoryCache.get(key);
}
public static int calculateInSampleSize(BitmapFactory.Options options,
int reqWidth) {
// 源图片的宽度
final int width = options.outWidth;
int inSampleSize = 1;
if (width reqWidth) {
// 计较呈现实宽度和方针宽度的比率
final int widthRatio = Math.round((float) width / (float) reqWidth);
inSampleSize = widthRatio;
}
return inSampleSize;
}
public static Bitmap decodeSampledBitmapFromResource(String pathName,
int reqWidth) {
// 第一次解析将inJustDecodeBounds设置为true,来取得图片巨细
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(pathName, options);
// 挪用下面界说的方式计较inSampleSize值
options.inSampleSize = calculateInSampleSize(options, reqWidth);
// 应用取得到的inSampleSize值再次解析图片
options.inJustDecodeBounds = false;
return BitmapFactory.decodeFile(pathName, options);
}
}
这里我们将ImageLoader类设成单例,并在机关函数中初始化了LruCache类,把它的最大缓存容量设为最大可用内存的1/8。然后又供应了其它几个方式能够支配LruCache,和对图片中止紧缩和读取。接上去新建MyScrollView担当自ScrollView,代码以下所示:
[java] view plaincopypublic class MyScrollView extends ScrollView implements OnTouchListener {
/**
* 每页要加载的图片数目
*/
public static final int PAGE_SIZE = 15;
/**
* 记实以后已加载到第几页
*/
private int page;
/**
* 每列的宽度
*/
private int columnWidth;
/**
* 以后第一列的高度
*/
private int firstColumnHeight;
/**
* 以后第二列的高度
*/
private int secondColumnHeight;
/**
* 以后第三列的高度
*/
private int thirdColumnHeight;
/**
* 是不是已加载过一次layout,这里onLayout中的初始化只需加载一次
*/
private boolean loadOnce;
/**
* 对图片中止办理的东西类
*/
private ImageLoader imageLoader;
/**
* 第一列的构造
*/
private LinearLayout firstColumn;
/**
* 第二列的构造
*/
private LinearLayout secondColumn;
/**
* 第三列的构造
*/
private LinearLayout thirdColumn;
/**
* 记实一切正鄙人载或等待下载的任务。
*/
private static Set LoadImageTask taskCollection;
/**
* MyScrollView下的直接子构造。
*/
private static View scrollLayout;
/**
* MyScrollView构造的高度。
*/
private static int scrollViewHeight;
/**
* 记实上垂直标的目的的转动距离。
*/
private static int lastScrollY = -1;
/**
* 记实一切界面上的图片,用以能够随时节制对图片的释放。
*/
private List ImageView imageViewList = new ArrayList ImageView
/**
* 在Handler中中止图片可见性查抄的断定,和加载更多图片的支配。
*/
private static Handler handler = new Handler() {
public void handleMessage(android.os.Message msg) {
MyScrollView myScrollView = (MyScrollView) msg.obj;
int scrollY = myScrollView.getScrollY();
// 若是以后的转动位置和前次不异,暗示已遏制转动
if (scrollY == lastScrollY) {
// 当转动的最底部,而且以后没有正鄙人载的任务时,起头加载下一页的图片
if (scrollViewHeight + scrollY = scrollLayout.getHeight()
taskCollection.isEmpty()) {
myScrollView.loadMoreImages();
}
myScrollView.checkVisibility();
} else {
lastScrollY = scrollY;
Message message = new Message();
message.obj = myScrollView;
// 5毫秒后再次对转动位置中止断定
handler.sendMessageDelayed(message, 5);
}
};
};
/**
* MyScrollView的机关函数。
*
* @param context
* @param attrs
*/
public MyScrollView(Context context, AttributeSet attrs) {
super(context, attrs);
imageLoader = ImageLoader.getInstance();
taskCollection = new HashSet LoadImageTask
setOnTouchListener(this);
}
/**
* 中止一些关头性的初始化支配,取得MyScrollView的高度,和取得第一列的宽度值。并在这里起头加载第一页的图片。
*/
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
if (changed !loadOnce) {
scrollViewHeight = getHeight();
scrollLayout = getChildAt(0);
firstColumn = (LinearLayout) findViewById(R.id.first_column);
secondColumn = (LinearLayout) findViewById(R.id.second_column);
thirdColumn = (LinearLayout) findViewById(R.id.third_column);
columnWidth = firstColumn.getWidth();
loadOnce = true;
loadMoreImages();
}
}
/**
* 监听用户的触屏事务,若是用户手指分开屏幕则起头中止转动检测。
*/
@Override
public boolean onTouch(View v, MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_UP) {
Message message = new Message();
message.obj = this;
handler.sendMessageDelayed(message, 5);
}
return false;
}
/**
* 起头加载下一页的图片,每张图片城市开启一个异步线程往下载。
*/
public void loadMoreImages() {
if (hasSDCard()) {
int startIndex = page * PAGE_SIZE;
int endIndex = page * PAGE_SIZE + PAGE_SIZE;
if (startIndex Images.imageUrls.length) {
Toast.makeText(getContext(), 正在加载... , Toast.LENGTH_SHORT)
.show();
if (endIndex Images.imageUrls.length) {
endIndex = Images.imageUrls.length;
}
for (int i = startIndex; i endIndex; i++) {
LoadImageTask task = new LoadImageTask();
taskCollection.add(task);
task.execute(Images.imageUrls[i]);
}
page++;
} else {
Toast.makeText(getContext(), 已没有更多图片 , Toast.LENGTH_SHORT)
.show();
}
} else {
Toast.makeText(getContext(), 未发现SD卡 , Toast.LENGTH_SHORT).show();
}
}
/**
* 遍历imageViewList中的每张图片,对图片的可见性中止查抄,若是图片已分开屏幕可见范围,则将图片替代成一张空图。
*/
public void checkVisibility() {
for (int i = 0; i imageViewList.size(); i++) {
ImageView imageView = imageViewList.get(i);
int borderTop = (Integer) imageView.getTag(R.string.border_top);
int borderBottom = (Integer) imageView
.getTag(R.string.border_bottom);
if (borderBottom getScrollY()
borderTop getScrollY() + scrollViewHeight) {
String imageUrl = (String) imageView.getTag(R.string.image_url);
Bitmap bitmap = imageLoader.getBitmapFromMemoryCache(imageUrl);
if (bitmap != null) {
imageView.setImageBitmap(bitmap);
} else {
LoadImageTask task = new LoadImageTask(imageView);
task.execute(imageUrl);
}
} else {
imageView.setImageResource(R.drawable.empty_photo);
}
}
}
/**
* 断定手机是不是有SD卡。
*
* @return 有SD卡前往true,没有前往false。
*/
private boolean hasSDCard() {
return Environment.MEDIA_MOUNTED.equals(Environment
.getExternalStorageState());
}
/**
* 异步下载图片的任务。
*
* @author guolin
*/
class LoadImageTask extends AsyncTask String, Void, Bitmap {
/**
* 图片的URL地址
*/
private String mImageUrl;
/**
* 可重复应用的ImageView
*/
private ImageView mImageView;
public LoadImageTask() {
}
/**
* 将可重复应用的ImageView传进
*
* @param imageView
*/
public LoadImageTask(ImageView imageView) {
mImageView = imageView;
}
@Override
protected Bitmap doInBackground(String... params) {
mImageUrl = params[0];
Bitmap imageBitmap = imageLoader
.getBitmapFromMemoryCache(mImageUrl);
if (imageBitmap == null) {
imageBitmap = loadImage(mImageUrl);
}
return imageBitmap;
}
@Override
protected void onPostExecute(Bitmap bitmap) {
if (bitmap != null) {
double ratio = bitmap.getWidth() / (columnWidth * 1.0);
int scaledHeight = (int) (bitmap.getHeight() / ratio);
addImage(bitmap, columnWidth, scaledHeight);
}
taskCollection.remove(this);
}
/**
* 依照传进的URL,对图片中止加载。若是这张图片已存在于SD卡中,则直接从SD卡里读取,不然就从搜集上下载。
*
* @param imageUrl
* 图片的URL地址
* @return 加载到内存的图片。
*/
private Bitmap loadImage(String imageUrl) {
File imageFile = new File(getImagePath(imageUrl));
if (!imageFile.exists()) {
downloadImage(imageUrl);
}
if (imageUrl != null) {
Bitmap bitmap = ImageLoader.decodeSampledBitmapFromResource(
imageFile.getPath(), columnWidth);
if (bitmap != null) {
imageLoader.addBitmapToMemoryCache(imageUrl, bitmap);
return bitmap;
}
}
return null;
}
/**
* 向ImageView中添加一张图片
*
* @param bitmap
* 待添加的图片
* @param imageWidth
* 图片的宽度
* @param imageHeight
* 图片的高度
*/
private void addImage(Bitmap bitmap, int imageWidth, int imageHeight) {
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
imageWidth, imageHeight);
if (mImageView != null) {
mImageView.setImageBitmap(bitmap);
} else {
ImageView imageView = new ImageView(getContext());
imageView.setLayoutParams(params);
imageView.setImageBitmap(bitmap);
imageView.setScaleType(ScaleType.FIT_XY);
imageView.setPadding(5, 5, 5, 5);
imageView.setTag(R.string.image_url, mImageUrl);
findColumnToAdd(imageView, imageHeight).addView(imageView);
imageViewList.add(imageView);
}
}
/**
* 找到此时应当添加图片的一列。准绳就是对三列的高度中止断定,以后高度最小的一列就是应当添加的一列。
*
* @param imageView
* @param imageHeight
* @return 应当添加图片的一列
*/
private LinearLayout findColumnToAdd(ImageView imageView,
int imageHeight) {
if (firstColumnHeight = secondColumnHeight) {
if (firstColumnHeight = thirdColumnHeight) {
imageView.setTag(R.string.border_top, firstColumnHeight);
firstColumnHeight += imageHeight;
imageView.setTag(R.string.border_bottom, firstColumnHeight);
return firstColumn;
}
imageView.setTag(R.string.border_top, thirdColumnHeight);
thirdColumnHeight += imageHeight;
imageView.setTag(R.string.border_bottom, thirdColumnHeight);
return thirdColumn;
} else {
if (secondColumnHeight = thirdColumnHeight) {
imageView.setTag(R.string.border_top, secondColumnHeight);
secondColumnHeight += imageHeight;
imageView
.setTag(R.string.border_bottom, secondColumnHeight);
return secondColumn;
}
imageView.setTag(R.string.border_top, thirdColumnHeight);
thirdColumnHeight += imageHeight;
imageView.setTag(R.string.border_bottom, thirdColumnHeight);
return thirdColumn;
}
}
/**
* 将图片下载到SD卡缓存起来。
*
* @param imageUrl
* 图片的URL地址。
*/
private void downloadImage(String imageUrl) {
HttpURLConnection con = null;
FileOutputStream fos = null;
BufferedOutputStream bos = null;
BufferedInputStream bis = null;
File imageFile = null;
try {
URL url = new URL(imageUrl);
con = (HttpURLConnection) url.openConnection();
con.setConnectTimeout(5 * 1000);
con.setReadTimeout(15 * 1000);
con.setDoInput(true);
con.setDoOutput(true);
bis = new BufferedInputStream(con.getInputStream());
imageFile = new File(getImagePath(imageUrl));
fos = new FileOutputStream(imageFile);
bos = new BufferedOutputStream(fos);
byte[] b = new byte[1024];
int length;
while ((length = bis.read(b)) != -1) {
bos.write(b, 0, length);
bos.flush();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (bis != null) {
bis.close();
}
if (bos != null) {
bos.close();
}
if (con != null) {
con.disconnect();
}
} catch (IOException e) {
e.printStackTrace();
}
}
if (imageFile != null) {
Bitmap bitmap = ImageLoader.decodeSampledBitmapFromResource(
imageFile.getPath(), columnWidth);
if (bitmap != null) {
imageLoader.addBitmapToMemoryCache(imageUrl, bitmap);
}
}
}
/**
* 取得图片的当地存储途径。
*
* @param imageUrl
* 图片的URL地址。
* @return 图片的当地存储途径。
*/
private String getImagePath(String imageUrl) {
int lastSlashIndex = imageUrl.lastIndexOf( / );
String imageName = imageUrl.substring(lastSlashIndex + 1);
String imageDir = Environment.getExternalStorageDirectory()
.getPath() + /PhotoWallFalls/ ;
File file = new File(imageDir);
if (!file.exists()) {
file.mkdirs();
}
String imagePath = imageDir + imageName;
return imagePath;
}
}
}
那我们就要来看一看loadMoreImages()方式的外部细节了。在这个方式中,应用了一个轮回来加载这一页中的每张图片,每次城市开启一个LoadImageTask,用于对图片中止异步加载。然后在LoadImageTask中,起首会先查抄一下这张图片能否是已存在于SD卡中了,若是还没存在,就从搜集上下载,然后把这张图片存放在LruCache中。接着将这张图按照必然的比例中止紧缩,并找出以后高度最小的一列,把紧缩后的图片添加出来便能够了。
别的,为了包管照片墙上的图片都能够或许合适地被收受接收,这里还插手了一个可见性查抄的方式,即checkVisibility()方式。这个方式的焦点思惟就是查抄今朝照片墙上的一切图片,断定出哪些是可见的,哪些是不成见。然后将那些不成见的图片都替代成一张空图,多么便能够包管法式不断不会占用太高的内存。当这些图片又从头变成可见的时辰,只需求再从LruCache中将这些图片从头掏出便可。若是某张图片已从LruCache中被移除,就会开启一个LoadImageTask,将这张图片从头加载到内存中。
然后翻开或新建activity_main.xml,在外面设置好瀑布流的构造格式,以下所示:
[html] view plaincopycom.example.photowallfallsdemo.MyScrollView xmlns:android= //schemas.android.com/apk/res/android
android:id= @+id/my_scroll_view
android:layout_width= match_parent
android:layout_height= match_parent
LinearLayout
android:layout_width= match_parent
android:layout_height= wrap_content
android:orientation= horizontal
LinearLayout
android:id= @+id/first_column
android:layout_width= 0dp
android:layout_height= wrap_content
android:layout_weight= 1
android:orientation= vertical
/LinearLayout
LinearLayout
android:id= @+id/second_column
android:layout_width= 0dp
android:layout_height= wrap_content
android:layout_weight= 1
android:orientation= vertical
/LinearLayout
LinearLayout
android:id= @+id/third_column
android:layout_width= 0dp
android:layout_height= wrap_content
android:layout_weight= 1
android:orientation= vertical
/LinearLayout
/LinearLayout
/com.example.photowallfallsdemo.MyScrollView
能够看到,这里我们应用了适才编写好的MyScrollView作为根构造,然后在外面放进了一个直接子构造LinearLayout用于统计以后滑动构造的高度,然后在这个构造下又添加了三个等宽的LinearLayout分别作为第一列、第二列和第三列的构造,多么在MyScrollView中便能够静态地向这三个LinearLayout里添加图片了。最初,由于我们应用到了搜集和SD卡存储的功用,是以还需求在AndroidManifest.xml中添加以下权限:
[html] view plaincopyuses-permission android:name= android.permission.WRITE_EXTERNAL_STORAGE /
uses-permission android:name= android.permission.INTERNET /
多么我们一切的编码任务就已完成了,此刻能够检验考试运转一下,结果以下图所示:
瀑布流方式的照片墙果真很是美不雅吧,并且由于我们有很是圆满的资本释放机制,不管你在照片墙上添加了几多图片,法式占用内存不断城市连结在一个公允的范围内。
好了,明天的讲授到此竣事,有疑问的伴侣请鄙人面留言。
上一篇:View类和自界说控件与实例
扫码关注微信公众号了解更多详情
跟技术大咖,专业导师一起交流学习