概述
经过一年的安卓学习,最后要结束了,整理一下,学过的资料包含《疯狂安卓讲义》,《安卓群英传》,《安卓高级开发》,《Android多媒体开发高级编程》和一些平常搜集到的知识点
编写这个博客主要是怀念安卓开发,知识量巨多,观看需谨慎,但若所有的知识点你都很清楚,那你安卓开发也是挺棒的
若你是搜索问题,搜到了这个页面建议Ctrl+F搜索一下,说不定真的有方案解决你的问题
animated-vector
<?xml version="1.0" encoding="utf-8"?>
<!--
android:drawable 引用绘图资源 vector
-->
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android" android:drawable="@drawable/ic_content_cut_black_24dp">
<!--
android:animation 引用动画 objectAnimator
android:name引用分段 path name
-->
<target
android:animation="@animator/my"
android:name="test"/>
</animated-vector>
添加水波纹效果
android:clickable="true"//确保可以点击
android:background="?attr/selectableItemBackground"//添加效果
绘制顶点
注意关闭硬件加速 纹理放到画笔中
float[] vs={0,300,300,300,300,0};
float[] ts={0,300,300,300,300,0};
//必须点与 上面的数量相同
int[] colors={Color.RED,Color.GREEN,Color.BLUE,Color.RED,Color.GREEN,Color.BLUE};
canvas.drawVertices(Canvas.VertexMode.TRIANGLES,6,vs,0,ts,0,colors,
0,null,0,0,paint);
常用快捷键
Ctrl+E 显示最近内容
Ctrl+Shift+上下箭头 交换两行
Ctrl+B 鼠标位于变量 快速搜索变量使用
Ctrl+P 查看方法参数
Ctrl+[+/-] 展开/折叠代码
右键查找使用(查看哪里使用了)
右键断点设置条件断点
断点页面可以添加各种断点 例如异常断点发生异常会停止
按住alt 框选代码
对于Xml同样可以抽取样式
Shift+A 快速搜索
Ctrl+H 查看类结构
可以在tool中找到以前的监视工具
使用vitamio做万能播放器
先引入mdule(推荐使用以前成功过的)
然后参照自己的项目修改vitamio的gradle信息
添加对应权限
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
使用前必须初始化 Vitamio.isInitialized(this); 其他使用与VideoView或MediaPlayer一样(其提供了与默认安卓一致的API)
组合组件的使用
public class Sidebar extends LinearLayout {// 注意继承的类 必须是布局的最外布局
private RecyclerView show;
public Sidebar(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
LayoutInflater.from(context).inflate(R.layout.view_sidebar,this,true);//加载布局 这里需要附加(注意参数)
show=findViewById(R.id.rv_show);//可以正常获取组件使用
}
}
对于复杂,重复布局可以使用include解耦
RecyclerView可以借助viewType属性显示不同页面
@Override
public int getItemViewType(int position) {//需要在Adapter中根据不同位置设置不同类型(重写该方法)
return Math.min(position,3);
}
DrawerLayout(侧滑)使用
对内部组件设置android:layout_gravity="<gravity>"会给对于组件添加滑动功能方向取决于设置的gravity,没有设置的会做为主内容,也可以使用代码控制开闭,添加监听
BottomNavigationView使用
<com.google.android.material.bottomnavigation.BottomNavigationView
android:layout_gravity="bottom"//设置导航位置
app:itemIconTint="@color/bnv"//设置图标颜色 这里颜色使用的是选择器 否则选择没有颜色变换
app:itemTextColor="@color/bnv"//设置文本颜色
app:labelVisibilityMode="labeled"//显示所有标题
android:background="?android:attr/windowBackground"
app:menu="@menu/navigation"//指定显示的菜单资源
android:id="@+id/bnv_show"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
Fragment
注意getView()用于获取对应的View必须在onCreateView()调用后才能获取对象View
使用fragmentManager.beginTransaction().replace(R.id.fl_show,fragment).commitNowAllowingStateLoss();会导致上面的整个生命周期都走一遍,会销毁View,但是数据不会销毁
官方推荐使用setArguments与getArguments传递信息(实际就是其内部的一个变量值)
小建议
可以抽取公用元素进行复用(封装)
若嵌套内部组件没有正常工作可以尝试使用容器包裹一层
善用RecyclerView可以显示多个不同子元素的特点
对于分离的视图组件推荐传递Context能够获取更多对象
onDraw注意要触发重绘事件才绘制 postInvalidate(非本线程) invalidate(本线程触发)
merge标签用于包裹布局文件,生成布局文件后会自动消失
修正GridView嵌套使用只显示一行
public class CorrectionGridView extends GridView {
public CorrectionGridView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public CorrectionGridView(Context context) {
super(context);
}
public CorrectionGridView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
@Override// 需要继承该类 重写 测量方法
public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int expandSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2, MeasureSpec.AT_MOST);
super.onMeasure(widthMeasureSpec, expandSpec);
}
}
对于抽取的ViewHolder可以使用工厂模式(其对于的View一般固定,只是修改样式属性)
public static GridViewHolder getInstance(Context context){// 使用静态方法进行构建
View view = LayoutInflater.from(context).inflate(R.layout.view_grid, null);//自处理固定逻辑
return new GridViewHolder(view,context);
}
private GridViewHolder(View view,Context context) {//隐藏构造方法
super(view);
gridView=view.findViewById(R.id.gv_show);
this.context=context;
}
居中对齐
可以让窄的上下都与宽的对齐,再设置android:gravity=“center”,若要调整背景可以调整layout_margin
黄油刀注意
若View与注解标明对象是同一个对象可以直接使用ButterKnife.bind(this);
若不是同一个需要指定注解使用对象与视图对象ButterKnife.bind(this,itemView);
@OnClick({R.id.btn_unknown,R.id.btn_3day,R.id.btn_5day,R.id.btn_delete})//可以指定多个事件源,通过id判断
使对象均匀分布
使用ConstraintLayout(弹簧布局)左右互相连接
LinearLayout嵌套match_parent失效
推荐使用RelativeLayout解决失效问题
多个请求调用判断是那个响应回调
根据返回值结果判断,例如使用APIweb项目,可以添加tag标记,从返回结果中获取tag标记
安卓真机连接到电脑服务器
注意ip地址是ipconfig的 无线局域网适配器 WLAN 的ipv4地址
对于API27及以上需要设置允许不加密内容传输(否则必须使用https)
在xml文件夹下创建 xxx.xml配置文件
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<base-config cleartextTrafficPermitted="true" />
</network-security-config>
在AndroidManifest.xml引用该配置文件
<application
android:networkSecurityConfig="@xml/network_security_config"//你的配置文件名
android:usesCleartextTraffic="true"
设置状态栏颜色
getWindow().setStatusBarColor(0xfffb7299);
设置屏幕旋转方向
setRequestedOrientation(portrait?ActivityInfo.SCREEN_ORIENTATION_PORTRAIT:ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
监听Activity配置改变自己处理改变防止重写创建
默认Activity配置改变会重新创建(例如屏幕旋转)
可以在配置文件中指明自己处理的改变 android:configChanges=“orientation|screenSize”
@Override//在该回调中处理配置改变
public void onConfigurationChanged(@NonNull Configuration newConfig) {
super.onConfigurationChanged(newConfig);//必须调用父类方法
}
设置指定EditText获取焦点并弹出输入键盘
input.setFocusable(true);
input.setFocusableInTouchMode(true);
input.requestFocus();
getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE);
使用对应的LayoutParams参数用于强制调整布局
RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(width, height);
params.leftMargin= (int) (-x/metrics.scaledDensity);
show.setLayoutParams(params);
OkHttpUtils可以通过设置请求Id,区分不同的请求
OkHttpUtils.get().url("").id(2233).build().execute(new StringCallback() {
@Override// 请求时可以设置id
public void onError(Call call, Exception e, int id) {
}//发生错误 或 响应时 会返回 id可以根据id判断请求 谁发送的
@Override
public void onResponse(String response, int id) {
}
});
request.addParams()//注意底层使用的map存储 因此无法传递数组参数(会互相覆盖)
View绘图
推荐使用getWidth()与getHeight()获取宽高,且一般在绘图时才能获取到值
WebView直接显示指定html内容
WebView.loadData(<html内容>,"text/html", "utf-8");
BitmapFactory.Options
设置图片解码设置,inXXX对输入进行设置,outXXX对输出进行设置,inJustDecodeBounds设置只获取图片信息并不加载
通过ExifInterface操作指定图片属性
ExifInterface anInterface = new ExifInterface("/sdcard/赵信.jpg");
可以设置获取属性 anInterface.getThumbnailBitmap();//获取图片缩略图
选择图片功能
选择意图:startActivityForResult(new Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI),2233);
在回调中获取图片的URI并解析为Bitmap
Uri data1 = data.getData();
Bitmap bitmap = BitmapFactory.decodeStream(getContentResolver().openInputStream(data1));//推荐要关闭流
在图片上绘制(注意直接加载的图片无法修改)
Canvas canvas = new Canvas(bitmap);// 使用bitmap构建Canvas进行绘图,会直接作用到原bitmap
Matrix操作
setXXX()会覆盖矩阵数据 postXXX()矩阵后乘 preXXX()矩阵前乘
设置绘图混合模式
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.ADD));// 设置绘图混合默认
绘图使用自定义字体
Typeface asset = Typeface.createFromAsset(getAssets(), "en_gd.ttf");
paint.setTypeface(asset);
音乐Service中更新播放UI
private static Test4Activity activity;
//全局设置 创建设置为自己 销毁设置为null(垃圾回收) 调整ui时判断Test4Activity是否为null再调用
public static void setActivity(Test4Activity activity) {
MusicService.activity = activity;//相当于 若自己存在就注册自己,不存在取消注册 类似广播
}
MediaPlayer设置缓冲监听
mediaPlayer.setOnBufferingUpdateListener((mp, percent) -> {
// 百分比 数据
show.setSecondaryProgress(mp.getDuration()*percent/100);
});
使用意图简单录音
startActivityForResult(new Intent(MediaStore.Audio.Media.RECORD_SOUND_ACTION),2233);
Uri uri = data.getData();
player.setDataSource(this,uri);// 类似 图片 使用 意图录音
获取视频封面
URI:MediaStore.Video.Thumbnails.EXTERNAL_CONTENT_URI
MediaStore.Video.Thumbnails.DATA路径字段(byte[]与音乐等路径类似)
使用遮罩过滤
paint.setMaskFilter(new BlurMaskFilter(5, BlurMaskFilter.Blur.INNER));//设置过滤
new BlurMaskFilter(5, BlurMaskFilter.Blur.INNER);//模糊过滤 模糊半径 模糊类型
new EmbossMaskFilter(new float[]{1,1,1},5,0.5f,5);//浮雕效果 光源方向 环境光强度 反射等级 模糊程度
使用颜色过滤
paint.setColorFilter(new ColorMatrixColorFilter(matrix));// 设置过滤
ColorMatrix matrix = new ColorMatrix(new float[]{// 颜色矩阵过滤
2,0,0,0,0, // 控制红色 红色=oldRed*2+oldGreen*0+oldBlue*0+oldAlpha*0+0
0,1,0,0,0, //控制 绿色
0,0,1,0,0,//控制 蓝色
0,0,0,1,0// 控制透明度
});// 设置到画笔上
new LightingColorFilter(1,23);//所有像素先乘前面的 再加后面的 会被限制到 0-255
new PorterDuffColorFilter(Color.RED, PorterDuff.Mode.ADD);//指定与颜色的混合模式绘制
动画Drawable
创建的动画Drawable都可以通过image.setImageLevel(level);(0-10000);设置动画进度
注意若依赖下载失败查看是否自己开了代理
使用IjkMediaPlayer播放本地flv
添加依赖(其他省略了)
android {
defaultConfig {
ndk {
abiFilters "armeabi-v7a", "arm64-v8a", "x86", "armeabi"
}
}
sourceSets {
main {
jniLibs.srcDirs = ['libs']
}
}
}
dependencies {
implementation 'tv.danmaku.ijk.media:ijkplayer-java:0.8.8'
implementation 'tv.danmaku.ijk.media:ijkplayer-armv7a:0.8.8'
api 'tv.danmaku.ijk.media:ijkplayer-exo:0.8.4'
}
使用类似MediaPlayer(需要结合SurfaceView)
操控剪切板
ClipboardManager manager= getSystemService(ClipboardManager.class);//获取剪切板服务
if(manager.hasPrimaryClip()){// 判断是否有文本
ClipData data = manager.getPrimaryClip();//获取文本
}else {//有获取内容 没有设置内容
manager.setPrimaryClip(ClipData.newPlainText("text","HelloWorld"));//设置文本
}
使用Notification构建前台
Notification build = new Notification.Builder(this)
.setContentTitle("标题")
.setContentText("内容")
.setLargeIcon(BitmapFactory.decodeResource(getResources(), R.drawable.icon))
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)//设置显示等级(锁屏也显示)
.setSmallIcon(R.drawable.icon2)
.setColor(0xfb7299)
.addAction(new Notification.Action(R.drawable.icon, "Ok", null))//使用广播 控制
.addAction(new Notification.Action(R.drawable.icon2, "Ok", null))
.addAction(new Notification.Action(R.drawable.icon3, "Ok", null))
.setStyle(new Notification.MediaStyle())//设置媒体播放样式
.build();
NotificationManager manager= getSystemService(NotificationManager.class);
manager.notify(2233,build);//显示前台
桌面小组件开发
推荐直接使用系统创建的进行改进,可以使用Handler进行循环调用
private Handler handler=new Handler(){// 使用Handler 定时调用
@Override
public void handleMessage(@NonNull Message msg) {
AppWidgetManager manager = AppWidgetManager.getInstance(context);// 注意保存context
ComponentName name = new ComponentName(context, MyAppWidgetProvider.class);//可以获取各种更新所需的信息
int[] appWidgetIds = manager.getAppWidgetIds(name);
onUpdate(context,manager,appWidgetIds);
handler.sendEmptyMessageDelayed(2233,1000);
}
};
//views.setOnClickPendingIntent 通过 添加点击意图 监听点击事件
创建集合类小组件
创建集合数据适配器服务(注意该服务必须有android:permission="android.permission.BIND_REMOTEVIEWS"权限)
public class MyViewsFactory extends RemoteViewsService implements RemoteViewsService.RemoteViewsFactory {
private Context context;
private int[] images;
public MyViewsFactory(Context context) {
this.context = context;
}
@Override
public void onCreate() {//创建回调
images=new int[]{R.drawable.test,R.drawable.test,R.drawable.test,R.drawable.test,R.drawable.test};
}
@Override
public int getCount() {//返回数据数量
return images.length;
}
@Override
public RemoteViews getViewAt(int position) {// 返回 设置好的视图
RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.image_iten);
views.setImageViewResource(R.id.iv_show,images[position%4]);
views.setTextViewText(R.id.tv_show,"HelloWorld");
return views;
}
@Override
public RemoteViews getLoadingView() {//返回加载中视图
return null;
}
@Override
public int getViewTypeCount() {
return 1;
}
@Override// 返回数据服务对象
public RemoteViewsFactory onGetViewFactory(Intent intent) {
return new MyViewsFactory(context);
}
}
static void updateAppWidget(Context context, AppWidgetManager appWidgetManager,int appWidgetId) {//更新视图时
RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.list_test);
Intent intent = new Intent(context,MyRemoteViewsService.class);// 指定数据服务
views.setRemoteAdapter(R.id.lv_show,intent);//使用意图指定适配器
views.setEmptyView(R.id.lv_show,R.layout.new_app_widget);//设置集合为空的显示
appWidgetManager.updateAppWidget(appWidgetId, views);
}
创建图标长按快捷方式
<shortcuts xmlns:android="http://schemas.android.com/apk/res/android"><!--快捷方式的xml配置文件-->
<shortcut
android:enabled="true"
android:icon="@drawable/test"
android:shortcutId="new_photo"
android:shortcutShortLabel="@string/app_name"><!--注意这里的文本必须全部使用引用-->
<intent
android:action="android.intent.action.VIEW"
android:targetClass="com.sk.androidadvancedprogram.MainActivity"
android:targetPackage="com.sk.androidadvancedprogram"/>
</shortcut><!--对应的意图-->
</shortcuts>
<meta-data
android:name="android.app.shortcuts"
android:resource="@xml/shortcut" /><!--加到启动Activity中-->
ShortcutManager manager= (ShortcutManager) getSystemService(SHORTCUT_SERVICE);
Intent intent = new Intent(this, Test2Activity.class);
intent.setAction(Intent.ACTION_VIEW);
//构建信息
ShortcutInfo info = new ShortcutInfo.Builder(this, "MyId")
.setShortLabel("动态添加的")
.setIcon(Icon.createWithResource(this, R.drawable.icon3))
.setIntent(intent)
.build();
//动态添加
manager.setDynamicShortcuts(Collections.singletonList(info));
//manager.requestPinShortcut(info,null); 添加快捷方式到桌面
//根据id移除
// manager.removeDynamicShortcuts(Collections.singletonList(""));
开发视频壁纸
继承WallpaperService编写逻辑
@Override
public void onVisibilityChanged(boolean visible) {//监听可见性
super.onVisibilityChanged(visible);
if(visible){ //可见时播放
player.start();
}else{
player.pause();//不可见时暂停
}
}
@Override
public void onSurfaceCreated(SurfaceHolder holder) {
super.onSurfaceCreated(holder);
player.setSurface(holder.getSurface());//注意设置Surface 不要player.setDisplay(holder);
}
注册Service(略)
//请求设置壁纸 默认很多手机没有设置壁纸的按钮 壁纸实现类全类名
ComponentName componentName = new ComponentName(getPackageName(), MyWallpaperService.class.getName());
Intent intent = new Intent("android.service.wallpaper.CHANGE_LIVE_WALLPAPER");
intent.putExtra("android.service.wallpaper.extra.LIVE_WALLPAPER_COMPONENT", componentName);
startActivityForResult(intent, 0);
弹幕使用
引入依赖:implementation ‘com.github.ctiao:dfm:0.4.2’
组件:DanmakuSurfaceView(推荐使用性能更好,但是无法透明显示,若需要设置透明度需要使用DanmakuView)
danmaku.setCallback(new DrawHandler.Callback() {// 准备
@Override
public void prepared() {
danmaku.start();
}
@Override
public void updateTimer(DanmakuTimer timer) {
}
@Override
public void danmakuShown(BaseDanmaku danmaku) {
}
@Override
public void drawingFinished() {
}
});
context = DanmakuContext.create();//获取上下文对象发送弹幕要用
danmaku.enableDanmakuDrawingCache(true); //提升屏幕绘制效率
danmaku.prepare(new BaseDanmakuParser() {// 暂时使用普通的弹幕解析器
@Override
protected IDanmakus parse() {
return new Danmakus();
}
}, context); //进行弹幕准备
//创建弹幕对象,TYPE_SCROLL_RL表示从左向右滚动的弹幕
BaseDanmaku danmaku = context.mDanmakuFactory.createDanmaku(BaseDanmaku.TYPE_SCROLL_RL);
danmaku.text = content;
danmaku.padding = 6;
danmaku.textSize = 30;
danmaku.textColor = Color.WHITE; //弹幕文字颜色
danmaku.time = this.danmaku.getCurrentTime();
if(border)danmaku.borderColor = Color.BLUE; //弹幕文字边框的颜色
this.danmaku.addDanmaku(danmaku); //添加一条弹幕
onMeasure支持wrap_content或match_parent
int wSize = MeasureSpec.getSize(widthMeasureSpec);
int wMode = MeasureSpec.getMode(widthMeasureSpec);
if(wMode==MeasureSpec.AT_MOST){// AT_MOST 是 wrap_content
wSize=size;
}
setMeasuredDimension(wSize,hSize);// 最终设置测量值
ViewGroup通常不需要绘制(若没有设置背景颜色,甚至onDarw方法不会被调用)
自定义View声明xml属性
在attrs.xml中声明属性
<declare-styleable name="MyView"><!--可以使用名字重用-->
<attr name="color" format="color|reference"/><!--格式可以使用多个 |分割-->
</declare-styleable>
构造器获取属性
TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.MyView);//指定名字 可以多次获取不同名的
color = array.getColor(R.styleable.MyView_color, Color.GREEN);
array.recycle();//最后一定要回收
自定义ViewGroup
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
measureChildren(widthMeasureSpec,heightMeasureSpec);//先测量子容器 测量完毕Child的getMeasuredXXX才有值
// 测试自己 略
}
@Override//注意child.layout设置的坐标相对父容器 传入的是父容器在父容器中的位置信息
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int childCount = getChildCount();
int top=0;
for (int i = 0; i < childCount; i++) {// 布局子容器
View child = getChildAt(i);//设置孩子位置 设置完getWidth等才有值 MeasuredWidth获取的包含外边距 getWidth没有
child.layout(l,top,l+child.getMeasuredWidth(),top+child.getMeasuredHeight());
top+=child.getMeasuredHeight();
}
}
Scroller使用
// 开始位置 相对偏移
scroller.startScroll(getScrollX(),getScrollY(),-getScrollX(),-getScrollY());// 设置滚动
postInvalidate();//触发滚动
@Override// 绘制时会调用该方法
public void computeScroll() {
if(scroller.computeScrollOffset()){// 若更新成功
// 绝对滚动 滚动是针对 内部坐标的 并不移动 View位置
scrollTo(scroller.getCurrX(),scroller.getCurrY());
postInvalidate();//调用绘制 循环更新 直到完毕
}
}
ListView拾遗
android:divider="@color/colorPrimary" //指定分割线的颜色
android:dividerHeight="1dp" //指定分割线的高度
android:divider="@null" //取消分割线
android:scrollbars="none" //取消滚动条
show.smoothScrollToPosition(1);// 指定滑动到那一条
show.setEmptyView(empty);//设置没有数据时的显示 有数据自动隐藏
show.setOnScrollListener(new AbsListView.OnScrollListener() {// 监听滑动事件
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
}
@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
}
});
show.setTextFilterEnabled(true);//开启文本过滤 默认关闭 (只对ArrayAdapter有效)
show.clearTextFilter(); //清除过滤词
show.setFilterText(query); //设置过滤词
安卓坐标系
Canvas常用方法
canvas.save();//状态压栈
canvas.restore();//状态出站
canvas.translate(300,300);//常用画布变换方法
canvas.scale(1,1);
canvas.rotate(i*30);
Bitmap像素操作
// 目标数组 偏移 每行偏移 开始位置 与范围
bitmap.getPixels(datas,0,width,0,0,width,height);// 加载像素信息到datas数组
// 注意 目标 图片必须有透明通道(因为 颜色处理会添加透明通道)
bitmap= Bitmap.createBitmap(width,height, Bitmap.Config.ARGB_8888);
bitmap.setPixels(datas,0,width,0,0,width,height);//设置像素信息 对象必须可修改
drawBitmapMesh
//按指定纹理坐标绘制图片 图片 注意宽高是多少块(少一) 顶点位置 先横后竖 先x后y(具体位置,不是0-1) 偏移 颜色相关参数 类似顶点
canvas.drawBitmapMesh(bitmap,WIDTH-1,HEIGHT-1,vers,0,null,0,paint);
使用setXfermode混合模式
canvas.drawBitmap(back,0,0,paint);
int i = canvas.saveLayer(0, 0, 720, 405, paint);//必须保存恢复图层
canvas.drawBitmap(mask,0,0,paint);//绘制遮罩
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_OUT));//使用混合模式(也有其他的可以使用)
canvas.drawBitmap(front,0,0,paint);
paint.setXfermode(null);// 取消使用
canvas.restoreToCount(i);//恢复
属性动画,非属性动画
TranslateAnimation animation = new TranslateAnimation(0, 0, 0, 600);//非属性动画 不影响事件
// 属性名(set加不加都行) 若没有对应的set属性可以继承 封装set属性
ObjectAnimator.ofFloat(view,"TranslationY",0,600).setDuration(3000).start();// 属性动画 影响事件响应位置
ValueAnimator所有动画的基类
ValueAnimator animator = ValueAnimator.ofArgb(Color.RED, Color.GREEN, Color.BLUE);//指定关键帧
animator.setDuration(3000);//设置持续时间
animator.setInterpolator(new AccelerateInterpolator());//设置差值器
animator.setEvaluator(new ArgbEvaluator());//设置值解析器 方便生成各种对象
animator.addUpdateListener(animation -> {//监听动画进行计算出的数据 并使用
});
animator.start();//开始动画
View简单动画
view.animate().alpha(0).setDuration(3000);//设置完毕立马生效 xxx() xxxBy() 前者绝对,后者相对
为布局添加动画
LayoutAnimationController controller = new LayoutAnimationController(animation);//animation
show.setLayoutAnimation(controller);//show ViewGroup 设置子对象添加或删除时的动画
// 也可以直接在布局中指定动画文件 android:layoutAnimation="@anim/layout_anin"
继承Animation自定义动画
public class MyAnimation extends Animation {//继承 Animation自定义动画
@Override//初始化方法
public void initialize(int width, int height, int parentWidth, int parentHeight) {
super.initialize(width, height, parentWidth, parentHeight);
setDuration(3000);//初始化设置
setFillAfter(true);
setInterpolator(new BounceInterpolator());
camera=new Camera();
centerX=width/2;
centerY=height/2;
}
private Camera camera;
private int centerX;
private int centerY;
@Override//传入动画对象的变换矩阵 与当前进度
protected void applyTransformation(float interpolatedTime, Transformation t) {
Matrix matrix = t.getMatrix();
camera.save();//保存相机
camera.rotateY(180*interpolatedTime);//根据进度旋转
camera.getMatrix(matrix);//设置变换信息
camera.restore();//恢复相机 方便复用
matrix.preTranslate(centerX,centerY);
matrix.postTranslate(-centerX,-centerY);
}
}
安卓生命周期
Intent常用Flag
Intent.FLAG_ACTIVITY_NEW_TASK 创建新栈放启动的Activity
Intent.FLAG_ACTIVITY_SINGLE_TOP 相当于该Activity的launchMode为singleTop
Intent.FLAG_ACTIVITY_CLEAR_TOP 相当于该Activity的launchMode为singleTask
Intent.FLAG_ACTIVITY_NO_HISTORY 启动的Activity若启动其他Activity无法通过返回键返回
获取所有应用信息
List<ApplicationInfo> infos = manager.getInstalledApplications(PackageManager.GET_UNINSTALLED_PACKAGES);
ApplicationInfo.loadLabel(manager);//获取标签 ApplicationInfo.loadIcon(manager);//获取图标 其他应用信息获取省略
使用ViewStub延迟加载View
<ViewStub
android:id="@+id/vs_show"
android:layout="@layout/show_itme" //指定要加载的布局文件
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
show.setVisibility(View.VISIBLE);//两种方式 都会触发视图加载
LinearLayout inflate = (LinearLayout) show.inflate();//不过这个可以返回根布局
Palette通过Bitmap获取配色方案
Palette.from(bitmap).generate(palette -> {
Palette.Swatch swatch = palette.getLightMutedSwatch();//用于获取解析到的配色方案
show.setBackgroundColor(swatch.getRgb());//获取其中一种颜色
});
tint(着色)
<ImageView
android:tintMode="screen" //指定混合模式
android:tint="@color/colorPrimary" //指定着色
android:src="@drawable/test"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
裁剪View
show.setOutlineProvider(new ViewOutlineProvider() {//可以给任意View或嵌套View添加裁剪
@Override
public void getOutline(View view, Outline outline) {
outline.setOval(0,0,720, 720);//设置裁剪规则 还可以设置其他裁剪规则
}
});
show.setClipToOutline(true);//开启裁剪 false关闭裁剪
Activity跳转动画
startActivity(new //启动修改
Intent(this,Test2Activity.class),ActivityOptions.makeSceneTransitionAnimation(this).toBundle());
getWindow().requestFeature(Window.FEATURE_CONTENT_TRANSITIONS);//目标页面开启动画效果 必须在设置View之前设置
Slide explode = new Slide();//配置动画 默认有 Slide Fade Explode可以使用
explode.setDuration(1000);
getWindow().setExitTransition(new Explode());//退出当前页面的动画
getWindow().setEnterTransition(explode);//进入当前页面的动画
getWindow().setReenterTransition(explode);//返回到上个页面的动画(上个页面设置)
getWindow().setReturnTransition(explode);//返回到上个页面的动画(当前页面设置)
Activity带组件跳转
<ImageView
android:transitionName="img" //跳转到界面或该界面都需要有对应的组件并设置相同的transitionName
android:src="@drawable/test"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
startActivity(new Intent(this,Test3Activity.class),ActivityOptions.makeSceneTransitionAnimation(this, show,
"img").toBundle());//show组件应用 "img"组件的transitionName
ViewAnimationUtils构建环形动画
// 动画对象 开始位置 开始,结束半径
ViewAnimationUtils.createCircularReveal(show, (int) event.getX(), (int) event.getY(),0, 1000);//返回动画对象
Notification补充
Notification notification = new Notification.Builder(this)
.setSmallIcon(R.drawable.icon)
.setContentTitle("标题")
.setContentText("内容")
.setAutoCancel(true)//设置点击后自动消失
.setVisibility(Notification.VISIBILITY_PRIVATE)//设置显示等级(锁屏是否显示)
.setFullScreenIntent(PendingIntent.getActivity(this, 2233,//设置为全屏通知
new Intent(this, MainActivity.class), Intent.FILL_IN_ACTION), true)
.build();
manager.notify(2233,notification);//若Id相同不会累加会进行更新
ViewDragHelper使用
public MyLinearLayout(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
helper = ViewDragHelper.create(this, new ViewDragHelper.Callback() {
@Override// 是否响应child的拖拽事件 pointerId手指Id
public boolean tryCaptureView(@NonNull View child, int pointerId) {
return true;
}
@Override
public int clampViewPositionHorizontal(View child, int left, int dx){
if(left<0)left=0;
return left;
}// 拖动边界控制 child 对应组件 left,top 当前left或top dx,dy当前移动量
@Override
public int clampViewPositionVertical(View child, int top, int dy){
if(top<0)top=0;
return top;
}
@Override// 释放监听 View对象 x,y轴速度
public void onViewReleased(@NonNull View releasedChild, float xvel, float yvel) {
helper.smoothSlideViewTo(releasedChild,0,0);// 移动指定组件到指定位置
postInvalidate();//开启动画
}
@Override//拖拽边界监听 边界类型 手指Id
public void onEdgeDragStarted(int edgeFlags, int pointerId) {
}
// 还有其他事件回调 可以监听
});
helper.setEdgeTrackingEnabled(ViewDragHelper.EDGE_ALL);//设置监听拖拽的边界
}
private ViewDragHelper helper;
@Override
public boolean onInterceptTouchEvent(MotionEvent event){
return helper.shouldInterceptTouchEvent(event);
}
// 对拖拽事件进行处理
@Override
public boolean onTouchEvent(MotionEvent event){
helper.processTouchEvent(event);
return true;
}
@Override
public void computeScroll() {//同样有动画 需要computeScroll循环调用
if(helper.continueSettling(true)){
postInvalidate();
}
}
Visualizer分析音频
visualizer = new Visualizer(player.getAudioSessionId());//获取MediaPlayer音频SessionId(注意必须有录音权限否则报错)
visualizer.setCaptureSize(Visualizer.getCaptureSizeRange()[1]);
visualizer.setDataCaptureListener(new Visualizer.OnDataCaptureListener() {
@Override
public void onWaveFormDataCapture(Visualizer visualizer, byte[] waveform, int samplingRate) {
}
@Override// 音频数据回调
public void onFftDataCapture(Visualizer visualizer, byte[] fft, int samplingRate) {
model[0] = (byte) Math.abs(fft[0]);//model长 512 处理数据为对应格式
for (int i = 2, j = 1; j < model.length;i+=2,j++) {
model[j] = (byte) Math.hypot(fft[i], fft[i + 1]);
}
}
},Visualizer.getMaxCaptureRate() / 2, true, true);
visualizer.setEnabled(true);//设置开启或关闭
visualizer.release();//最后不要忘了释放资源
visualizer=null;
VolumeShaper控制音量
VolumeShaper.Configuration build = new VolumeShaper.Configuration.Builder()
.setInterpolatorType(VolumeShaper.Configuration.INTERPOLATOR_TYPE_LINEAR)//线性插值
.setCurve(new float[]{0, 0.5f, 1}, new float[]{0, 1, 0.5f})//时间(总时间比例)与音量(原始音量比例)映射
.setDuration(3000)//持续时间
.build();//构建
VolumeShaper volumeShaper = player.createVolumeShaper(build);
volumeShaper.apply(VolumeShaper.Operation.PLAY);//播放完毕会把音量调整到最后的倍数 多次调用效果会累加
使用MediaProjection进行屏幕投影
startActivityForResult(MediaProjectionManager.createScreenCaptureIntent(),2233);//使用意图请求
MediaProjection=MediaProjectionManager.getMediaProjection(resultCode,data);
//意图响应成功 设置显示的Surface 及大小信息
VirtualDisplay=MediaProjection.createVirtualDisplay("Test",720,1280,1,DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,show.getHolder().getSurface(),null,null);
if(display!=null){//最后不要忘了释放资源
display.release();
display=null;
}
if(projection!=null){
projection.stop();
projection=null;
}
aar包
arr包其实就是带res的jar包
使用activity-alias给activity起别名,动态激活改变图标
<activity-alias
android:name="com.sunnews.dodo.SplashAliasActivity" //别名 名字随意
android:enabled="false" //默认启用主的,这个不启用
android:icon="@drawable/icon" //这里设置更改图标
android:label="新消息!Do" // 设置更改后的标题
android:targetActivity=".MainActivity"> //实际代理类
<intent-filter>//启动类必须的
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity-alias>
PackageManager pm = getPackageManager();//动态设置组件激活状态,用于改变名字与图标(也可以动态设置其它组件的激活状态)
pm.setComponentEnabledSetting(new ComponentName(this, "com.sk.androidmadness.MainActivity"),
PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP);
pm.setComponentEnabledSetting(new ComponentName(this, "com.sunnews.dodo.SplashAliasActivity"),
PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP);
ConstraintLayout使用
layout_constraintXXX_toXXXOf 指定上下左右对齐,使用对方的id若与父控件对齐使用parent,一排可以相连,实现均匀布局
圆形布局
app:layout_constraintCircle="@id/btn_1" //指定圆心
app:layout_constraintCircleAngle="180" //指定角度
app:layout_constraintCircleRadius="100dp" //指定半径
百分比设置
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintHeight_percent="0.1" //使用百分比布局 注意 对应的layout_height必须设置为0dp
app:layout_constraintWidth_percent="0.5"
比例布局
app:layout_constraintDimensionRatio="1" //指定宽高比例
android:layout_width="wrap_content" //按不为0的一方为标准 不能都为0或都不为0
android:layout_height="0dp"
指定偏移
app:layout_constraintHorizontal_bias="0.1" //指定水平或垂直偏移,对应方向上必须有两个链条绑定两边
app:layout_constraintVertical_bias="0.2"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintRight_toRightOf="parent"
安卓上下文菜单
registerForContextMenu(btn);// 为组件长按事件注册菜单
@Override// 可以根据 不同组件响应不同菜单
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
getMenuInflater().inflate(R.menu.my,menu);
}
BroadcastReceiver的生命周期
若使用xml静态注册,每次接收广播,onReceive()执行完毕,都会销毁对象
若使用Java代码,动态注册,不会反复创建销毁
MediaPlayer注意
只有setDataSource()的才需要prepare(),直接通过MediaPlayer.create()不能调用prepare()
若设置setLooping(true);完成回调将不会调用
prepare()或prepareAsync();准备完毕都会回调setOnPreparedListener()方法
处于stop状态可以调用prepare()或prepareAsync()重新进入准备状态,不需要再次设置数据源
OpenGL ES纹理过滤(使用glTexParameterf设置)
GL_NEAREST(临近过滤):取最近的像素点,小图片放大会有颗粒感
GL_LINEAR(线性过滤):根据临近像素进行差值,放大会模糊,不会有颗粒感
OpenGL ES透明效果
gl.glDisable(GL10.GL_DEPTH_TEST); //关闭深度测试 必须关闭深度测试否则会出现问题
gl.glEnable(GL10.GL_BLEND); //打开混合
gl.glBlendFunc(GL10.GL_SRC_ALPHA, GL10.GL_ONE_MINUS_SRC_ALPHA); //使用alpha通道值
WebView补充
暂停,开始与销毁(建议同Activity的生命周期)
WebView.onResume(); //暂停 页面不再刷新响应
WebView.onPause(); //恢复
WebView.destroy(); //释放资源
执行指定JS代码,返回字符串结果(这个不刷新页面,直接使用loadUrl()执行JS需要刷新页面)
evaluateJavascript("window.alert('OKOKKKKKKKKKK')", value -> Log.e("TEST",value));
设置一个意图对应多个应用时选择的界面名称
startActivity(Intent.createChooser(new Intent(Intent.ACTION_SET_WALLPAPER),"选择壁纸"));//为意图选择设置标题
AppWidgetProvider
AppWidgetProvider继承BroadcastReceiver在onReceive中会根据ACTION不同字段触发不同方法(可以根据它发送不同广播对其操作)
注意,若需要重写onReceive()一定要先调用父类的
View属性扩展
elevation:指定组件高度,改变默认遮挡关系(可以代码动态改变遮挡关系)
translationZ:效果与elevation类似,不过elevation是绝对高度,translationZ是相对高度
TableLayout使用
<TableLayout
android:collapseColumns="1" // 指定 隐藏列
android:shrinkColumns="0" //指定指定列可以收缩 下标从0开始
android:stretchColumns="1,2" //指定列可以扩展 都可以指定多个
android:layout_width="match_parent"
android:layout_height="match_parent">
<TableRow><Button/><Button/><Button/></TableRow>
</TableLayout>
安卓文本链接
<TextView
android:autoLink="phone" // 指定为电话链接 点击会自动拨到电话
android:text="1008311"
android:layout_below="@id/tl_show"
style="@style/MyText"/>
// 对指定TextView添加链接 匹配模式 默认会直接把内容拼接到后面
Linkify.addLinks(textView, Pattern.compile("\d+"),"https://www.baidu.com/s?wd=");
AnalogClock
<AnalogClock
android:hand_minute="@drawable/hand" //分针 会以中间做旋转点,建议一半透明
android:hand_hour="@drawable/hand" //时针
android:dial="@drawable/bg" //背景
android:layout_below="@id/tv_show"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
ImageSwitcher与TextSwitcher
ImageSwitcher与TextSwitcher都继承自ViewSwitcher
通过setFactory设置创建View(只创建一次,且要创建对应类型的视图)
setOutAnimation,setInAnimation设置内容改变时的动画,通过设置图片资源(ImageSwitcher)获取文本资源(TextSwitcher)改变内容
PopupWindow使用
PopupWindow window = new PopupWindow(this);//创建
window.setContentView(getLayoutInflater().inflate(R.layout.test_menu,null));//设置显示视图
window.setWidth(ViewGroup.LayoutParams.WRAP_CONTENT);//设置宽高 必须设置 view 宽高 才能显示
window.setHeight(ViewGroup.LayoutParams.WRAP_CONTENT);
window.showAsDropDown(btn);//指定显示位置(指定View的下面,有指定偏移与对齐的重载)
PopupMenu使用
PopupMenu menu = new PopupMenu(this, btn2);//创建 并指定锚点对象
menu.inflate(R.menu.test);//加载菜单文件
menu.show();//显示
AsyncTask使用
new AsyncTask<Integer, Integer, String>() {// 参数依次是 初始参数类型 更新UI传递数据类型 最终结果类型
@Override
protected String doInBackground(Integer... integers) {//处理耗时工作
publishProgress(2233);//发布后台进度 调用onProgressUpdate更新前台
return "OKKKK";
}
@Override
protected void onPreExecute() {//执行前调用
}
@Override
protected void onPostExecute(String s) {//执行结束调用
}
@Override
protected void onProgressUpdate(Integer... values) {//用于更新进度
}
}.execute(100);//开始工作
PreferenceActivity创建设置
Activity继承PreferenceActivity
onCreate中设置xml资源文件(PreferenceScreen) addPreferencesFromResource(R.xml.test);
也可以代码获取Preference对象 findPreference(“test3”);(通过key值查找)
<ListView
android:id="@android:id/list" //注意布局文件中必须有该id对应的ListView
android:layout_width="match_parent"
android:layout_height="match_parent"/>
常用设置元素(都需要key属性,用于设置保存的key必须唯一)
RingtonePreference 选择铃声
MultiSelectListPreference 多选(必须指定entryValues和entries用于设置数据源)
ListPreference 单选(同样需要指定entryValues和entries)
EditTextPreference 文本设置
SwitchPreference 开关设置
CheckBoxPreference 勾选设置
通过 PreferenceManager.getDefaultSharedPreferences(context); 获取对应map(context是随意的,获取默认的)
intent-filter设置
<intent-filter><!--可以配置多个action,请求意图匹配一个就行-->
<action android:name="android.intent.action.TEST1"/>
<action android:name="android.intent.action.TEST2"/>
<category android:name="android.intent.category.DEFAULT"/>
<!--配置数据格式与类型,必须满足才能启动没有配置的任意 例如这里需要 dataType为a/b url为sk://www.sk:80/path 注意需要使用setDataAndType同时设置,否则data与type互相覆盖 -->
<data android:mimeType="a/b" android:scheme="sk" android:host="www.sk" android:port="80" android:path="/path"/>
<!--对面可以通过getIntent().getData()获取url信息 属性也可以使用正则表达式指定-->
</intent-filter>
设置SeekBar的样式
android:progressDrawable="@drawable/test"//指定样式文件
<layer-list xmlns:android="http://schemas.android.com/apk/res/android"><!--样式文件-->
<!--指定背景与进度 主要注意id-->
<item android:id="@android:id/background" android:drawable="@drawable/hand"/>
<item android:id="@android:id/progress" android:drawable="@drawable/pro"/>
</layer-list>
ContentProvider使用
继承ContentProvider并注册
<provider
android:name=".part3.MyContentProvider"
android:authorities="com.sk.androidmadness.part3" <!--指定getContentResolver()操作时的uri-->
android:exported="true" /><!--指定向外开放-->
推荐聚合对应的SQLiteOpenHelper进行使用
UriMatcher的使用
matcher=new UriMatcher(UriMatcher.NO_MATCH);//创建并指定映射根uri的id 根uri就是只有域名没有路径的
matcher.addURI(BASE_URL,"cher",ALL);// 添加一个并指定id
//例如 com.sk.androidmadness.part3 "cher" 匹配 content://com.sk.androidmadness.part3/cher
matcher.addURI(BASE_URL,"cher/#",ONE);// 使用通配符#指定数字
int id=matcher.match(uri);//根据url返回匹配id
long id = ContentUris.parseId(uri);// 获取统配符id
Uri uriId = ContentUris.withAppendedId(uri, 12);// 为uri追加id
Uri noId = ContentUris.removeId(uriId); //移除id
若使用getContentResolver().registerContentObserver注册自己的ContentProvider,需要在自定义ContentProvider发生修改后手动调用getContext().getContentResolver().notifyChange();触发监听事件
IntentService
普通Service仍位于主线程不能执行耗时操作,若要执行耗时操作可以考虑使用IntentService
plurals复数资源文件
<plurals name="apple">
<item quantity="one">唯一一苹果</item>
<item quantity="zero">没有苹果</item>
<item quantity="other">%d个苹果</item><!--可以使用%d等引用传入变量-->
</plurals>
String quantityString = getResources().getQuantityString(R.plurals.apple, 0,0);//使用必须同时传入 数量与参数
在xml中使用对象
dataBinding {
enabled = true //首先build.gradle开启数据绑定
}
<layout><!--布局使用layout当外布局-->
<data><!--data内声明变量-->
<variable
name="user"
type="com.sk.androidadvancedprogram.part2.User" />
</data>
<LinearLayout ...>
<TextView
android:text="@{user.name}" <!--下面可以直接使用变量-->
style="@style/MyText"/>
<TextView
android:text='@{user.adress??"OK"}'<!--也可以为变量设置默认值(注意引号的使用)-->
style="@style/MyText"/>
</LinearLayout>
</layout>
//使用DataBindingUtil.setContentView设置布局文件代替原来的 setContentView
ActivityTest5BindingImpl binding = DataBindingUtil.setContentView(this, R.layout.activity_test5);
binding.setUser(user);//返回对象(根据xml动态生成的)设置为准确对象 会自动生成 set变量名 方法设置变量
意图预解析判断是否存在对应的组件处理
Intent intent = new Intent();
ComponentName name = intent.resolveActivity(getPackageManager());//若没有会返回null
// 获取所有匹配意图的 Activity信息
List<ResolveInfo> infos = getPackageManager().queryIntentActivities(intent, PackageManager.MATCH_ALL);
LocalBroadcastManager本地广播使用
manager = LocalBroadcastManager.getInstance(this);// 获取本地广播
manager.registerReceiver(receiver,new IntentFilter("TEST"));//注册取消注册
manager.unregisterReceiver(receiver);
manager.sendBroadcast(new Intent("TEST"));//发送本地广播
监听SharedPreferences的修改
preferences.registerOnSharedPreferenceChangeListener(listener);//为SharedPreferences添加监听
Handler补充
handler.sendEmptyMessageAtTime();//在指定时间发送
// 除了发送消息 也可以进行 任务调度
handler.postAtTime(runable,time);// 添加定时回调
handler.postDelayed(runable,time);// 添加延时回调
handler.removeCallbacks(runable);// 移除回调
Notification-MessagingStyle
setStyle(new Notification.MessagingStyle("Sk")// 设置显示信息-提示
.addMessage("OK1",System.currentTimeMillis()+2222,"LLLL")//每个 都是一条消息
.addMessage("OK2",System.currentTimeMillis()+4444,"LLLL")
.addMessage("OK3",System.currentTimeMillis()+6666,"LLLL"))
Notification自定义布局
RemoteViews views = new RemoteViews(getPackageName(),R.layout.remote);//加载布局
views.setProgressBar(R.id.pb_show,100,30,false);//修改布局
Notification build = new Notification.Builder(this)
.setContent(views)//设置布局信息
.setSmallIcon(R.drawable.test)
.setWhen(System.currentTimeMillis())
.build();
为Activity添加返回按钮
android:parentActivityName=".part2.Part2Activity"//在配置文件指定父Activity就行,不过要保留ActionBar
最后
以上就是心灵美水蜜桃为你收集整理的最强安卓笔记的全部内容,希望文章能够帮你解决最强安卓笔记所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复