首页
归档
留言
广告合作
友链
美女主播
Search
1
博瑞GE车机升级/降级
5,146 阅读
2
Mac打印机设置黑白打印
4,517 阅读
3
修改elementUI中el-table树形结构图标
4,516 阅读
4
Mac客户端添加腾讯企业邮箱方法
4,351 阅读
5
intelliJ Idea 2022.2.X破解
4,060 阅读
Java
HarmonyOS Next
Web前端
微信开发
开发辅助
App开发
数据库
随笔日记
登录
/
注册
Search
标签搜索
Spring Boot
Java
Spring Cloud
Mac
mybatis
WordPress
Nacos
Spring Cloud Alibaba
Mybatis-Plus
jQuery
Java Script
asp.net
微信小程序
Sentinel
UniApp
MySQL
asp.net core
IntelliJ IDEA
Jpa
树莓派
Laughing
累计撰写
570
篇文章
累计收到
1,424
条评论
首页
栏目
Java
HarmonyOS Next
Web前端
微信开发
开发辅助
App开发
数据库
随笔日记
页面
归档
留言
广告合作
友链
美女主播
搜索到
19
篇与
的结果
2022-01-31
UniApp Android客户端集成高德Web服务
问题最近使用uniapp做了一个记油耗的App。大家有兴趣的可以点击https://www.xiangcaowuyu.net/app/oil_note.html查看。在记油耗OilNote中,有一个根据用户地理位置获取附近加油站的功能,我这里使用的是高德地图的Web服务对应的周边搜索功能,搜索对应的poi。一开始,我主要是在微信小程序还有H5进行测试的,系统都能正确获取到附近的加油站信息。我以为既然是web服务,那么应该所有的平台都是一样的,事实证明,我还是太年轻了。我用两台手机测试的,一台是一加8(系统是Color OS 12)、另外一个华为Mate30 Pro(鸿蒙系统,具体版本不清楚),在一加手机,所有定位服务失效,在华为Mate30 Pro,时好时坏。解决其实现在问题解决了,我也不知道啥原因,解决方式也很简单,自己又在高德开放平台申请了Android的key,然后在HBuilder中配置上,重新用自己的证书打包就好了。1.申请Android平台的key2.配置Android平台的key修改mainfest.json文件,找到App模块配置,在Maps中,使用自己申请的Android平台的key3.使用自己的证书打包App在打包App时,使用自己的证书。注意包名要求高德开放平台填写的报名保持一致。
2022年01月31日
1,155 阅读
0 评论
2 点赞
2021-06-26
安卓ListView的item中含有EditText,动态添加item时EditText值的保存
EditText是ListView的item,ListView的item可以动态添加,从而让用户动态输入一些内容。ListView是依靠Adapter将View和数据联系起来的,实现动态添加item的效果,比较简单,只需数据源再加一个条目,比如list.add(…),然后Adapter调用notifyDataSetChanged方法刷新View即可。到这里,我们的关注点来了,我们最终是需要获取ListView中所有EditText的值,保存到本地或者提交至服务器的,那么如何做到呢?我们知道,ListView的每个item都对应着一个实体类对象,显然,这个实体类有一个属性,用于保存EditText的值,在Adapter的getView()中,又用于EditText值的显示。所以我们需要实刻保存EditText的值到这个实体类对象的属性里,不然当我们调用notifyDataSetChanged刷新视图后会发现,EditText的值为空或者错乱。正如下图的效果:要时刻保存EditText的值,我们需要给EditText设置一个文字改变时的监听器(addTextChangedListener),当文字发生改变后,我们获取EditText的值并存于itemObject的某个属性中。这里千万要脚下留心,在EditText调用setText之前,一定要把textChangedListener移除掉,否则setText后又会多次调用监听器里面的方法,造成值的清空。解决方法是每次getView时先remove掉监听器,再setText,最后再add监听器。适配器的代码如下:import android.content.Context; import android.text.Editable; import android.text.TextUtils; import android.text.TextWatcher; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.BaseAdapter; import android.widget.EditText; import java.util.List; /** * Created by zengd on 2016/8/17 0017. */ public class ListViewAdapter extends BaseAdapter { private List<ItemBean> mData; private Context mContext; public ListViewAdapter(Context mContext, List<ItemBean> mData) { this.mContext = mContext; this.mData = mData; } @Override public int getCount() { return mData.size(); } @Override public Object getItem(int position) { return mData.get(position); } @Override public long getItemId(int position) { return position; } @Override public View getView(int position, View convertView, ViewGroup parent) { ViewHolder holder = null; if (convertView == null) { convertView = LayoutInflater.from(mContext).inflate(R.layout.item_edittext, null); holder = new ViewHolder(convertView); convertView.setTag(holder); } else { holder = (ViewHolder) convertView.getTag(); } final ItemBean itemObj = mData.get(position); if (holder.editText.getTag() instanceof TextWatcher) { holder.editText.removeTextChangedListener((TextWatcher) holder.editText.getTag()); } holder.editText.setText(itemObj.getText()); TextWatcher watcher = new TextWatcher() { @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) { } @Override public void onTextChanged(CharSequence s, int start, int before, int count) { } @Override public void afterTextChanged(Editable s) { if (TextUtils.isEmpty(s)) { itemObj.setText(""); } else { itemObj.setText(s.toString()); } } }; holder.editText.addTextChangedListener(watcher); holder.editText.setTag(watcher); return convertView; } private class ViewHolder { private EditText editText; public ViewHolder(View convertView) { editText = (EditText) convertView.findViewById(R.id.edit_text); } } }由于我的实际项目中item里有三个EditText,在这里我简化成只有一个EditText。再多个EditText也是同理实现的。
2021年06月26日
1,088 阅读
0 评论
2 点赞
2021-06-12
uni-app基础之应用生命周期
uni-app 支持如下应用生命周期函数:函数名说明onLaunch当uni-app 初始化完成时触发(全局只触发一次)onShow当 uni-app 启动,或从后台进入前台显示onHide当 uni-app 从前台进入后台onError当 uni-app 报错时触发onUniNViewMessage对 nvue 页面发送的数据进行监听,可参考 nvue 向 vue 通讯onUnhandledRejection对未处理的 Promise 拒绝事件监听函数(2.8.1+)onPageNotFound页面不存在监听函数onThemeChange监听系统主题变化注意应用生命周期仅可在App.vue中监听,在其它页面监听无效。<script> export default { /** * */ onLaunch: function() { console.log('应用初始化完成后触发,全局只触发一次') }, /** * */ onShow: function() { console.log('App启动或者从后台重新进入前台时触发') }, /** * */ onHide: function() { console.log('App从前台进入后台时触发') }, /** * */ onError: function() { console.error('App报错时触发') }, /** * 对 nvue 页面发送的数据进行监听 */ onUniNViewMessage: () => { console.log('对 nvue 页面发送的数据进行监听') }, /** * */ onUnhandledRejection: () => { console.log('对未处理的 Promise 拒绝事件监听函数(2.8.1+)') }, /** * 页面不存在监听函数 */ onPageNotFound: () => { console.log('页面不存在时触发') }, /** * 监听系统主题变化 */ onThemeChange: () => { console.log('主题切换后触发') } } </script> <style> /*每个页面公共css */ </style>
2021年06月12日
1,222 阅读
0 评论
0 点赞
2021-04-15
通过手机热点解决Gradle下载太慢
网上也搜了很多解决办法,但是基本都没啥效果,包括替换阿里云的镜像。只要把笔记本的无线网, 连接上手机热点即可,那个速度飞快~~!!!2021.4.15亲测很好用, 每次我都是用这招解决的!! 不知道为什么, 但是我猜应该是手机网络不存在翻墙的因素吧,嘿嘿~~姑且不管什么原理,只要能解决问题的办法 就是好办法!!!
2021年04月15日
1,153 阅读
4 评论
25 点赞
2021-01-09
android EditText禁止键盘输入
最近在做项目,遇到一个需求,有一个文本框(EditText)存储重量信息,重量信息来源于蓝牙电子秤,所以禁止用户手工输入。如下图,回收物重量,能正常点击并出现光标,但是不会弹出软键盘实现定义一个常量类/** * Description: 反射方法名 * * @author : laughing * DateTime: 2021-01-07 13:11 */ public class ReflectMethodConstants { public static String SET_SHOW_SOFT_INPUT_ON_FOCUS = "setShowSoftInputOnFocus"; public static String SET_SOFT_INPUT_SHOWN_ON_FOCUS = "setSoftInputShownOnFocus"; }反射设置控件 //禁止弹出软键盘 Class<EditText> cls = EditText.class; Method method; try { method = cls.getMethod(ReflectMethodConstants.SET_SHOW_SOFT_INPUT_ON_FOCUS, boolean.class); method.setAccessible(true); method.invoke(viewHolder.textViewWeight, false); } catch (Exception ignored) { } try { method = cls.getMethod(ReflectMethodConstants.SET_SOFT_INPUT_SHOWN_ON_FOCUS, boolean.class); method.setAccessible(true); method.invoke(viewHolder.textViewWeight, false); } catch (Exception ignored) { } //禁止键盘输入 viewHolder.textViewWeight.setFilters(new InputFilter[]{ (source, start, end, dest, dStart, dEnd) -> { if (source.length() > 1) { return source; } return ""; } });
2021年01月09日
1,440 阅读
1 评论
3 点赞
2020-12-28
FloatingDragButton:炫酷的拖拽浮动按钮
IOS的Assistive Touch效果很炫酷,可以任意拖拽,同时点击后会展开菜单栏。然而,这不只是IOS的特权,Android也可以实现。但是由于悬浮窗需要申请权限,所以本文仅在app内实现,可以任意拖拽,并可以响应点击事件。一、效果图效果还是不错的。上图看出虽然没有像IOS一样弹出菜单栏,仅仅以Toast和旋转动画的效果代替了(因为太懒了,更炫酷的效果交给你们的想象了)。但是确实支持点击事件,并且和拖拽事件不冲突。Github地址:FloatingDragButton二、实现原理1、拖拽实现很简单,设置TouchListener监听,实现onTouch方法,在ACTION_MOVE的过程中随着x,y坐标的移动更新浮动按钮的位置。下面具体介绍重写onTouch方法的具体实现。监听ACTION_DOWN事件case MotionEvent.ACTION_DOWN:{ mDownPointerId = MotionEventCompat.getPointerId(event, 0); mPreviousX = event.getRawX(); mPreviousY = event.getRawY(); break; }记录初始的坐标以及触摸点。监听ACTION_MOVE事件case MotionEvent.ACTION_MOVE:{ if (mDownPointerId >= 0) { int index = MotionEventCompat.getActionIndex(event); int id = MotionEventCompat.getPointerId(event, index); if (id == mDownPointerId) { boolean update = adjustMarginParams(view, event); if (!update) { break; } mFloatView.requestLayout(); mHasMoved = true; result = true; } } break; }其中最重要的是adjustMarginParams(view, event)方法,来更新浮动按钮的相对位置。private boolean adjustMarginParams(View v, MotionEvent event) { float x = event.getRawX(); float y = event.getRawY(); float deltaX = x - mPreviousX; float deltaY = y - mPreviousY; if (!mHasMoved) { if (Math.abs(deltaX) < mTouchSlop && Math.abs(deltaY) < mTouchSlop) { return false; } } //左上角位置 int newX = (int)x - mFloatView.getWidth() / 2; int newY = (int)y - mFloatView.getHeight() / 2; newX = Math.max(newX, mBoundsInScreen.left + mEdgePaddingLeft); newX = Math.min(newX, mBoundsInScreen.right - mEdgePaddingRight - mFloatView.getWidth()); newY = Math.max(newY, mBoundsInScreen.top + mEdgePaddingTop); newY = Math.min(newY, mBoundsInScreen.bottom - mEdgePaddingBottom - mFloatView.getHeight()); mFloatViewWindowParam.x = newX; mFloatViewWindowParam.y = newY - mParentMarginTop; return true; }其中mBoundsInScreen代表浮动按钮可移动的矩形范围。根据当前event内的坐标与mBoundsInScreen范围比较,选择最终拖拽到达的位置,设置给浮动按钮的布局参数mFloatViewWindowParam,然后调用requestLayout更新布局。监听ACTION_UP/ACTION_CANCELcase MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL:{ if (mDownPointerId >= 0 && mHasMoved) { event.setAction(MotionEvent.ACTION_CANCEL); adjustMarginParams(view, event); mFloatView.requestLayout(); int center = (mBoundsInScreen.width() - mFloatView.getWidth()) / 2; int x = mFloatViewWindowParam.x; int destX = 0; int posX = Gravity.LEFT; //抬起时 根据位置强制把浮动按钮归于左边或右边 if (x < center) { // 左边 destX = mBoundsInScreen.left + mEdgePaddingLeft; } else { posX = Gravity.RIGHT; destX = mBoundsInScreen.right - mEdgePaddingRight - mFloatView.getWidth(); } if (mFloatButtonCallback != null) { float posY = 0; if (mBoundsInScreen.height() - mFloatView.getHeight() != 0) { posY = 1f * (mFloatViewWindowParam.y - mBoundsInScreen.top) / (mBoundsInScreen.height() - mFloatView.getHeight()); } mFloatButtonCallback.onPositionChanged(destX, mFloatViewWindowParam.y, posX, posY); } int deltaHorizon = destX - x; //小于100直接移动 否则开启动画 if (Math.abs(deltaHorizon) < 100) { mFloatViewWindowParam.x = destX; mFloatView.requestLayout(); } else { ValueAnimator animator = ValueAnimator.ofInt(x, destX); animator.setInterpolator(mInterpolator); if (mUpdateListener == null) { mUpdateListener = new FloatAnimatorUpdateListener(); mUpdateListener.setUpdateView(FloatTouchListener.this); } animator.addUpdateListener(mUpdateListener); animator.setDuration(200); animator.start(); } } resetStatus(); break; } }实现当抬起的瞬间,根据当前所处坐标靠左还是靠右,把浮动按钮置于左边缘或者右边缘。同时,调用回调,把移动相对位置传给回调函数,实现拖拽监听。当从当前位置移动到左/右边缘的距离小于100时,直接移动,否则实现动画减速移动效果。如此简单便可实现任意拖拽的效果了,具体一些细节要细看源码实现。2.点击实现也许有人会认为点击事件很好实现啊,setOnClickListener()设置个监听就可以实现了。不信你去试试,没用。其实点击实现才是本篇文章的精髓,因为灵活应用到了事件分发机制。从事件分发机制中我们知道,就优先级而言:onTouchListener>onClickListenr。上面的拖拽事件已经消费了onTouchListener(即onTouch方法中返回true),那么就不会下发到onClickListenr,自然就不会产生点击事件。也许你想让onTouchListener不消费,然后不就下发到onClickListenr了么?确实这样可以实现点击事件,但是拖拽功能又实现不了了。因为onTouch方法中返回false,确实onClickListenr接收到了事件,然后消费掉。可是因为onTouch方法中返回false,所以接下来的一切事件不能接受,没办法响应拖拽效果了。通过上面的分析,最终的解决办法就是:onTouch方法中,在接收到ACTION_DOWN后,返回false,交给onClickListenr处理。剩下的ACTION_MOVE/ACTION_UP等事件,返回true,交给onTouchListener处理,这样自然就可以既实现拖拽效果又实现点击效果了。具体实现以下面伪代码为例 public boolean onTouch(View view, MotionEvent event) { int action = MotionEventCompat.getActionMasked(event); if (mFloatButtonCallback != null) { mFloatButtonCallback.onTouch(); } boolean result = false; switch (action) { case MotionEvent.ACTION_DOWN:{ .................................................... break; } case MotionEvent.ACTION_MOVE:{ .................................................... result = true; break; } case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL:{ ................................................ break; } } return result; }原理就是这么简单,更加炫酷的效果可自定义实现,喜欢就点个star支持下吧!Github地址:https://github.com/LRH1993/FloatingDragButton
2020年12月28日
1,443 阅读
0 评论
6 点赞
2020-12-28
Android搜索控件SearchView
搜索框控件一.默认效果该控件可通过继承的方式来实现样式上的扩展。二.使用方法创建布局 <com.example.kylinarm.searchviewdemo.components.view.seach.KylinSearchView android:layout_width="match_parent" android:layout_height="wrap_content" android:padding="10dp" android:id="@+id/sv_default" > </com.example.kylinarm.searchviewdemo.components.view.seach.KylinSearchView>自定义属性<!-- 搜索框 --> <declare-styleable name="kylin_search_style"> <attr name="img_src" format="reference"/><!-- 图片地址 --> <attr name="img_size" format="dimension"/><!-- 图片大小 --> <attr name="img_visibility" format="boolean"/><!-- 图片显示/隐藏 --> <attr name="show_location" format="enum"> <enum name="left" value="0"/> <enum name="right" value="2"/> <enum name="centre" value="1"/> </attr> <attr name="edt_hint" format="string"/><!-- 提示文字 --> <attr name="edt_size" format="dimension"/><!-- 提示文字大小 --> <attr name="edt_hint_color" format="color"/><!-- 提示文字的颜色 --> <attr name="search_backgroup" format="reference"/><!-- 搜索框背景 --> <attr name="search_padding" format="reference"/><!-- 搜索框背景 --> </declare-styleable>例如 <com.example.kylinarm.searchviewdemo.components.view.seach.KylinSearchView android:layout_width="match_parent" android:layout_height="wrap_content" android:padding="10dp" android:id="@+id/sv_default" app:edt_hint = "搜索" app:edt_hint_color = "#336688" > </com.example.kylinarm.searchviewdemo.components.view.seach.KylinSearchView>使用方法KylinSearchView searchViewDemo = (KylinSearchView) findViewById(R.id.sv_default);监听搜索(默认软键盘回车搜索)searchViewDemo.setOnSearchListener(this); //实现OnSearchListener接口获取搜索框内容searchViewDemo.getSearchContent();清空搜索框searchViewDemo.clearSearch()让搜索框失去焦点searchViewDemo.lostRocus();执行搜索操作searchViewDemo.search();扩展封装时只定义了一些比较常用的方法,如果有其它需求,比如说搜索时改变图片图标或者文字颜色之类的,而组件内部并没用提供这些方法,为了考虑方便扩展,开发者可以从外部获取到搜索框、提示图标、输入框三个对象。searchViewDemo.getSearchEditText(); // 返回输入框 searchViewDemo.getSearchImageView(); // 返回图标 searchViewDemo.getSearchFrameView(); // 返回搜索框自定义样式可以通过继承的方式实现自定义样,比如说demo中的仿微信搜索框public class WxSearchView extends KylinSearchView{ private RelativeLayout searchFrame; private EditText edtSearch; private ImageView ivSearch; public WxSearchView(Context context) { super(context); } public WxSearchView(Context context, AttributeSet attrs) { super(context, attrs); } @Override public int getLayoutId() { return R.layout.layout_wx_search; } @Override public void initChildView() { searchFrame = seachView.findViewById(R.id.rl_content_wx); edtSearch = seachView.findViewById(R.id.edt_search_wx); ivSearch = seachView.findViewById(R.id.iv_search_wx); } @Override public ImageView getImageView() { return ivSearch; } @Override public EditText getEditText() { return edtSearch; } @Override public RelativeLayout getSearchFrame() { return searchFrame; } }子类要重写SearchExtendImpl接口的所有方法getImageView() 、 getEditText() 、 getSearchFrame() 需要分别返回三个对象给父类。三.代码文档https://www.jianshu.com/p/353a87f60bb6
2020年12月28日
1,926 阅读
0 评论
25 点赞
2020-12-28
android 连接蓝牙打印机 BluetoothAdapter
android 连接蓝牙打印机 BluetoothAdapter源码下载地址:https://github.com/yylxy/BluetoothText.gitpublic class PrintActivity extends AppCompatActivity { //设备列表 private ListView listView; private ArrayList<PrintBean> mBluetoothDevicesDatas; private PrintAdapter adapter; //蓝牙适配器 private BluetoothAdapter mBluetoothAdapter; //请求的code public static final int REQUEST_ENABLE_BT = 1; private Switch mSwitch; private FloatingActionButton mFloatingActionButton; private ProgressBar mProgressBar; private Toolbar toolbar; private TextView searchHint; /** * 启动打印页面 * * @param printContent 要打印的内容 */ public static void starUi(Context context, String printContent) { Intent intent = new Intent(context, PrintActivity.class); intent.putExtra("id", id); intent.putExtra("printContent", printContent); context.startActivity(intent); } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); //广播注册 IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_FOUND); filter.addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED); registerReceiver(mReceiver, filter); // Don't forget to unregister during onDestroy //初始化 mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); mSwitch = (Switch) findViewById(R.id.switch1); mFloatingActionButton = (FloatingActionButton) findViewById(R.id.floatingActionButton); mProgressBar = (ProgressBar) findViewById(R.id.progressBar3); toolbar = (Toolbar) findViewById(R.id.toolbar); searchHint = (TextView) findViewById(R.id.searchHint); toolbar.setTitle("选择打印设备"); listView = (ListView) findViewById(R.id.listView); mBluetoothDevicesDatas = new ArrayList<>(); String printContent=getIntent().getStringExtra("printContent"); adapter = new PrintAdapter(this, mBluetoothDevicesDatas, TextUtils.isEmpty(printContent)?"123456789完\n\n\n":printContent); listView.setAdapter(adapter); chechBluetooth(); addViewListener(); } /** * 判断有没有开启蓝牙 */ private void chechBluetooth() { //没有开启蓝牙 if (mBluetoothAdapter != null) { if (!mBluetoothAdapter.isEnabled()) { Intent intent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE); // 设置蓝牙可见性,最多300秒 intent.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 20); startActivityForResult(intent, REQUEST_ENABLE_BT); setViewStatus(true); //开启蓝牙 } else { searchDevices(); setViewStatus(true); mSwitch.setChecked(true); } } } /** * 搜索状态调整 * * @param isSearch 是否开始搜索 */ private void setViewStatus(boolean isSearch) { if (isSearch) { mFloatingActionButton.setVisibility(View.GONE); searchHint.setVisibility(View.VISIBLE); mProgressBar.setVisibility(View.VISIBLE); } else { mFloatingActionButton.setVisibility(View.VISIBLE); mProgressBar.setVisibility(View.GONE); searchHint.setVisibility(View.GONE); } } /** * 添加View的监听 */ private void addViewListener() { //蓝牙的状态 mSwitch.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { @Override public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { if (isChecked) { openBluetooth(); setViewStatus(true); } else { closeBluetooth(); } } }); //重新搜索 mFloatingActionButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if (mSwitch.isChecked()) { searchDevices(); setViewStatus(true); } else { openBluetooth(); setViewStatus(true); } } }); toolbar.setNavigationIcon(R.drawable.ic_arrow_back_black_24dp); toolbar.setNavigationOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { Toast.makeText(PrintActivity.this, "88", Toast.LENGTH_SHORT).show(); } }); } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); if (resultCode == RESULT_OK && requestCode == REQUEST_ENABLE_BT) { Log.e("text", "开启蓝牙"); searchDevices(); mSwitch.setChecked(true); mBluetoothDevicesDatas.clear(); adapter.notifyDataSetChanged(); } else if (resultCode == RESULT_CANCELED && requestCode == REQUEST_ENABLE_BT) { Log.e("text", "没有开启蓝牙"); mSwitch.setChecked(false); setViewStatus(false); } } /** * 打开蓝牙 */ public void openBluetooth() { Intent intent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE); // 设置蓝牙可见性,最多300秒 intent.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 20); startActivityForResult(intent, REQUEST_ENABLE_BT); } /** * 关闭蓝牙 */ public void closeBluetooth() { mBluetoothAdapter.disable(); } /** * 搜索蓝牙设备 */ public void searchDevices() { mBluetoothDevicesDatas.clear(); adapter.notifyDataSetChanged(); //开始搜索蓝牙设备 mBluetoothAdapter.startDiscovery(); } /** * 通过广播搜索蓝牙设备 */ private final BroadcastReceiver mReceiver = new BroadcastReceiver() { public void onReceive(Context context, Intent intent) { String action = intent.getAction(); // 把搜索的设置添加到集合中 if (BluetoothDevice.ACTION_FOUND.equals(action)) { BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); //已经匹配的设备 if (device.getBondState() == BluetoothDevice.BOND_BONDED) { addBluetoothDevice(device); //没有匹配的设备 } else { addBluetoothDevice(device); } adapter.notifyDataSetChanged(); //搜索完成 } else if (BluetoothAdapter.ACTION_DISCOVERY_FINISHED.equals(action)) { setViewStatus(false); } } /** * 添加数据 * @param device 蓝牙设置对象 */ private void addBluetoothDevice(BluetoothDevice device) { for (int i = 0; i < mBluetoothDevicesDatas.size(); i++) { if (device.getAddress().equals(mBluetoothDevicesDatas.get(i).getAddress())) { mBluetoothDevicesDatas.remove(i); } } if (device.getBondState() == BluetoothDevice.BOND_BONDED && device.getBluetoothClass().getDeviceClass() == PRINT_TYPE) { mBluetoothDevicesDatas.add(0, new PrintBean(device)); } else { mBluetoothDevicesDatas.add(new PrintBean(device)); } } }; }class PrintAdapter extends BaseAdapter { private ArrayList<PrintBean> mBluetoothDevicesDatas; private Context mContext; //蓝牙适配器 private BluetoothAdapter mBluetoothAdapter; //蓝牙socket对象 private BluetoothSocket mmSocket; private UUID uuid; //打印的输出流 private static OutputStream outputStream = null; //搜索弹窗提示 ProgressDialog progressDialog = null; private final int exceptionCod = 100; //打印的内容 private String mPrintContent; //在打印异常时更新ui Handler handler = new Handler() { @Override public void handleMessage(Message msg) { super.handleMessage(msg); if (msg.what == exceptionCod) { Toast.makeText(mContext, "打印发送失败,请稍后再试", Toast.LENGTH_SHORT).show(); if (progressDialog != null) { progressDialog.dismiss(); } } } }; /** * @param context 上下文 * @param mBluetoothDevicesDatas 设备列表 * @param printContent 打印的内容 */ public PrintAdapter(Context context, ArrayList<PrintBean> mBluetoothDevicesDatas, String printContent) { this.mBluetoothDevicesDatas = mBluetoothDevicesDatas; mContext = context; mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); mPrintContent = printContent; uuid = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB"); } public int getCount() { return mBluetoothDevicesDatas.size(); } @Override public Object getItem(int position) { return position; } @Override public long getItemId(int position) { return position; } @Override public View getView(final int position, View convertView, ViewGroup parent) { convertView = LayoutInflater.from(mContext).inflate(R.layout.itme, null); View icon = convertView.findViewById(R.id.icon); TextView name = (TextView) convertView.findViewById(R.id.name); TextView address = (TextView) convertView.findViewById(R.id.address); TextView start = (TextView) convertView.findViewById(R.id.start); final PrintBean dataBean = mBluetoothDevicesDatas.get(position); icon.setBackgroundResource(dataBean.getTypeIcon()); name.setText(dataBean.name); address.setText(dataBean.isConnect ? "已连接" : "未连接"); start.setText(dataBean.getDeviceType(start)); //点击连接与打印 convertView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { try { //如果已经连接并且是打印机 if (dataBean.isConnect && dataBean.getType() == PRINT_TYPE) { if (mBluetoothAdapter.isEnabled()) { new ConnectThread(mBluetoothAdapter.getRemoteDevice(dataBean.address)).start(); progressDialog = ProgressDialog.show(mContext, "提示", "正在打印...", true); } else { Toast.makeText(mContext, "蓝牙没有打开", Toast.LENGTH_SHORT).show(); } //没有连接 } else { //是打印机 if (dataBean.getType() == PRINT_TYPE) { setConnect(mBluetoothAdapter.getRemoteDevice(dataBean.address), position); //不是打印机 } else { Toast.makeText(mContext, "该设备不是打印机", Toast.LENGTH_SHORT).show(); } } } catch (Exception e) { e.printStackTrace(); } } }); return convertView; } /** * 匹配设备 * * @param device 设备 */ private void setConnect(BluetoothDevice device, int position) { try { Method createBondMethod = BluetoothDevice.class.getMethod("createBond"); createBondMethod.invoke(device); mBluetoothDevicesDatas.get(position).setConnect(true); notifyDataSetChanged(); } catch (Exception e) { e.printStackTrace(); } } /** * 发送数据 */ public void send(String sendData) { try { byte[] data = sendData.getBytes("gbk"); outputStream.write(data, 0, data.length); outputStream.flush(); outputStream.close(); progressDialog.dismiss(); } catch (IOException e) { e.printStackTrace(); handler.sendEmptyMessage(exceptionCod); // 向Handler发送消息,更新UI } } /** * 连接为客户端 */ private class ConnectThread extends Thread { public ConnectThread(BluetoothDevice device) { try { mmSocket = device.createRfcommSocketToServiceRecord(uuid); } catch (IOException e) { e.printStackTrace(); } } public void run() { //取消的发现,因为它将减缓连接 mBluetoothAdapter.cancelDiscovery(); try { //连接socket mmSocket.connect(); //连接成功获取输出流 outputStream = mmSocket.getOutputStream(); send(mPrintContent); } catch (Exception connectException) { Log.e("test", "连接失败"); connectException.printStackTrace(); //异常时发消息更新UI Message msg = new Message(); msg.what = exceptionCod; // 向Handler发送消息,更新UI handler.sendMessage(msg); try { mmSocket.close(); } catch (Exception closeException) { closeException.printStackTrace(); } return; } } } }
2020年12月28日
1,285 阅读
0 评论
23 点赞
2020-12-20
HeiPermission:一句代码搞定 Android M 动态权限检测
项目地址建议通过源码的方式引入。项目地址https://github.com/forJrking/HeiPermission使用方式导入permlib到Project中,app添加库依赖。(建议拷贝源码,可根据需求定制)奇淫技巧:checkPermission(CheckPermListener listener, int resString, String... mPerms) - listener: 权限全部通过接口回调,只检测没有后续行为可 null - resString: 权限用途的说明提示(引导用户开启权限) - mPerms: 申请的高危权限组(可同时申请多个)最简单的使用方式Activity中:首先需要检测权限的 Activity extends PermissionActivity checkPermission(new CheckPermListener() { @Override public void superPermission() { TODO : 需要权限去完成的功能 } },R.string.camera, Manifest.permission.CAMERA, Manifest.permission.WRITE_EXTERNAL_STORAGE);Fragment中:首先Fragment依存的 Activity extends PermissionActivit ((PermissionActivity)getActivity()).checkPermission( new PermissionActivity.CheckPermListener() { @Override public void superPermission() { TODO : 需要权限去完成的功能 } },R.string.camera, Manifest.permission.CAMERA);
2020年12月20日
1,235 阅读
0 评论
24 点赞
2020-12-14
XUpdate:一个轻量级、高可用性的Android版本更新框架.md
一个轻量级、高可用性的Android版本更新框架。赶紧点击使用说明文档,体验一下吧!在提issue前,请先阅读【提问的智慧】,并严格按照issue模板进行填写,节约大家的时间。在使用前,请一定要仔细阅读使用说明文档,重要的事情说三遍!!!在使用前,请一定要仔细阅读使用说明文档,重要的事情说三遍!!!在使用前,请一定要仔细阅读使用说明文档,重要的事情说三遍!!!X系列库快速集成为了方便大家快速集成X系列框架库,我提供了一个空壳模版供大家参考使用: https://github.com/xuexiangjys/TemplateAppProject特征支持post和get两种版本检查方式,支持自定义网络请求。支持设置只在wifi下进行版本更新。支持静默下载(后台更新)、自动版本更新。提供界面友好的版本更新提示弹窗,可自定义其主题样式。支持自定义版本更新检查器、版本更新解析器、版本更新提示器、版本更新下载器、版本更新安装、出错处理。支持MD5文件校验、版本忽略、版本强制更新等功能。支持自定义文件校验方法【默认是MD5校验】。支持自定义请求API接口。兼容Android6.0、7.0、8.0、9.0和10.0。支持中文和英文两种语言显示(国际化)。支持Flutter插件使用:flutter_xupdate。支持React-Native插件使用:react-native-xupdate。组成结构本框架借鉴了AppUpdate中的部分思想和UI界面,将版本更新中的各部分环节抽离出来,形成了如下几个部分:版本更新检查器IUpdateChecker:检查是否有最新版本。版本更新解析器IUpdateParser:解析服务端返回的数据结果。版本更新提示器IUpdatePrompter:展示最新的版本信息。版本更新下载器IUpdateDownloader:下载最新的版本APK安装包。网络请求服务接口IUpdateHttpService:定义了进行网络请求的相关接口。除此之外,还有两个监听器:版本更新失败的监听器OnUpdateFailureListener。版本更新apk安装的监听器OnInstallListener。更新调度核心:版本更新业务代理IUpdateProxy:负责版本更新的流程控制,调用update开始进行版本更新流程。更新流程调用update之后的流程:IUpdateProxy/XUpdate --- (update) ---> IUpdateChecker --->(请求服务器,获取最新版本信息)---> IUpdateParser ---> (解析服务器返回的数据,并构建UpdateEntity版本更新实体)---> IUpdateProxy ---> (如无最新版本,直接结束,否则进行下面流程) ---自动模式---> IUpdateDownloader ---> (下载最新的应用apk) ---> 安装应用 ---非自动模式---> IUpdatePrompter ---> 给出版本更新的提示 ---> 用户点击更新 ---> IUpdateDownloader ---> (下载最新的应用apk) ---> 跳转到应用安装界面 ---> 用户点击(取消或忽略) ---> 结束点击查看框架UML设计图Demo更新后台服务由于github最近访问比较慢,如果需要更好地体验XUpdate,你可以点击自己搭建一个简易的版本更新服务。Demo下载蒲公英下载蒲公英下载的密码: xuexiangjysGithub下载2、快速集成指南目前支持主流开发工具AndroidStudio的使用,直接配置build.gradle,增加依赖即可.2.1、Android Studio导入方法,添加Gradle依赖1.先在项目根目录的 build.gradle 的 repositories 添加:allprojects { repositories { ... maven { url "https://jitpack.io" } } }2.然后在dependencies添加:以下是版本说明,选择一个即可。androidx版本:2.0.0及以上dependencies { ... // androidx版本 implementation 'com.github.xuexiangjys:XUpdate:2.0.4' }support版本:1.1.6及以下dependencies { ... // support版本 implementation 'com.github.xuexiangjys:XUpdate:1.1.6' }2.2、初始化XUpdate在Application进行初始化配置:XUpdate.get() .debug(true) .isWifiOnly(true) //默认设置只在wifi下检查版本更新 .isGet(true) //默认设置使用get请求检查版本 .isAutoMode(false) //默认设置非自动模式,可根据具体使用配置 .param("versionCode", UpdateUtils.getVersionCode(this)) //设置默认公共请求参数 .param("appKey", getPackageName()) .setOnUpdateFailureListener(new OnUpdateFailureListener() { //设置版本更新出错的监听 @Override public void onFailure(UpdateError error) { if (error.getCode() != CHECK_NO_NEW_VERSION) { //对不同错误进行处理 ToastUtils.toast(error.toString()); } } }) .supportSilentInstall(true) //设置是否支持静默安装,默认是true .setIUpdateHttpService(new OKHttpUpdateHttpService()) //这个必须设置!实现网络请求功能。 .init(this); //这个必须初始化【注意】:如果出现任何问题,可开启debug模式来追踪问题。如果你还需要将日志记录在磁盘上,可实现以下接口XUpdate.get().setILogger(new ILogger() { @Override public void log(int priority, String tag, String message, Throwable t) { //实现日志记录功能 } });2.3、版本更新实体信息(1) UpdateEntity字段属性字段名类型默认值备注mHasUpdatebooleanfalse是否有新版本mIsForcebooleanfalse是否强制安装:不安装无法使用appmIsIgnorablebooleanfalse是否可忽略该版本mVersionCodeint0最新版本codemVersionNameStringunknown_version最新版本名称mUpdateContentString""更新内容mDownloadEntityDownloadEntity/下载信息实体mIsSilentbooleanfalse是否静默下载:有新版本时不提示直接下载mIsAutoInstallbooleantrue是否下载完成后自动安装(2) DownloadEntity字段属性字段名类型默认值备注mDownloadUrlString""下载地址mCacheDirString""文件下载的目录mMd5String""下载文件的加密校验值(默认使用md5加密),用于校验,防止下载的apk文件被替换(最新演示demo中有计算校验值的工具)mSizelong0下载文件的大小【单位:KB】mIsShowNotificationbooleanfalse是否在通知栏上显示下载进度(3) PromptEntity字段属性字段名类型默认值备注mThemeColorintR.color.xupdate_default_theme_color主题色(进度条和按钮的背景色)mTopResIdintR.drawable.xupdate_bg_app_top顶部背景图片资源idmButtonTextColorint0按钮文字颜色mSupportBackgroundUpdatebooleanfalse是否支持后台更新mWidthRatiofloat-1(无约束)版本更新提示器宽度占屏幕的比例mHeightRatiofloat-1(无约束)版本更新提示器高度占屏幕的比例2.4、文件加密校验方式本框架默认使用的文件加密校验方法是MD5加密方式,当然如果你不想使用MD5加密,你也可以自定义文件加密器IFileEncryptor,以下是MD5文件加密器的实现供参考:/** * 默认的文件加密计算使用的是MD5加密 * * @author xuexiang * @since 2019-09-06 14:21 */ public class DefaultFileEncryptor implements IFileEncryptor { /** * 加密文件 * * @param file * @return */ @Override public String encryptFile(File file) { return Md5Utils.getFileMD5(file); } /** * 检验文件是否有效(加密是否一致) * * @param encrypt 加密值, 如果encrypt为空,直接认为是有效的 * @param file 需要校验的文件 * @return 文件是否有效 */ @Override public boolean isFileValid(String encrypt, File file) { return TextUtils.isEmpty(encrypt) || encrypt.equalsIgnoreCase(encryptFile(file)); } } 最后再调用XUpdate.get().setIFileEncryptor方法设置即可生效。3、版本更新3.1、默认版本更新直接调用如下代码即可完成版本更新操作:XUpdate.newBuild(getActivity()) .updateUrl(mUpdateUrl) .update();需要注意的是,使用默认版本更新,请求服务器返回的json格式应包括如下内容:{ "Code": 0, //0代表请求成功,非0代表失败 "Msg": "", //请求出错的信息 "UpdateStatus": 1, //0代表不更新,1代表有版本更新,不需要强制升级,2代表有版本更新,需要强制升级 "VersionCode": 3, "VersionName": "1.0.2", "ModifyContent": "1、优化api接口。\r\n2、添加使用demo演示。\r\n3、新增自定义更新服务API接口。\r\n4、优化更新提示界面。", "DownloadUrl": "https://raw.githubusercontent.com/xuexiangjys/XUpdate/master/apk/xupdate_demo_1.0.2.apk", "ApkSize": 2048 "ApkMd5": "..." //md5值没有的话,就无法保证apk是否完整,每次都会重新下载。框架默认使用的是md5加密。 }3.2、自动版本更新自动版本更新:自动检查版本 + 自动下载apk + 自动安装apk(静默安装)。只需要设置isAutoMode(true),不过如果设备没有root权限的话,是无法做到完全的自动更新(因为静默安装需要root权限)。XUpdate.newBuild(getActivity()) .updateUrl(mUpdateUrl) .isAutoMode(true) //如果需要完全无人干预,自动更新,需要root权限【静默安装需要】 .update();3.3、支持后台更新开启支持后台更新后, 用户点击“后台更新”按钮后,就可以进入到后台更新,不用一直在更新界面等待.XUpdate.newBuild(getActivity()) .updateUrl(mUpdateUrl) .supportBackgroundUpdate(true) .update();3.4、强制版本更新就是用户不更新的话,程序将无法正常使用。只需要服务端返回UpdateStatus字段为2即可。当然如果你自定义请求返回api的话,只需要设置UpdateEntity的mIsForce字段为true即可。3.5、自定义版本更新提示弹窗的主题通过设置更新顶部图片、主题色、按钮文字颜色、宽高比率等来实现自定义主题样式.promptThemeColor: 设置主题颜色promptButtonTextColor: 设置按钮的文字颜色promptTopResId: 设置顶部背景图片promptWidthRatio: 设置版本更新提示器宽度占屏幕的比例,默认是-1,不做约束promptHeightRatio: 设置版本更新提示器高度占屏幕的比例,默认是-1,不做约束XUpdate.newBuild(getActivity()) .updateUrl(mUpdateUrl) .promptThemeColor(ResUtils.getColor(R.color.update_theme_color)) .promptButtonTextColor(Color.WHITE) .promptTopResId(R.mipmap.bg_update_top) .promptWidthRatio(0.7F) .update();3.6、自定义版本更新解析器实现IUpdateParser接口即可实现解析器的自定义。XUpdate.newBuild(getActivity()) .updateUrl(mUpdateUrl3) .updateParser(new CustomUpdateParser()) //设置自定义的版本更新解析器 .update(); public class CustomUpdateParser implements IUpdateParser { @Override public UpdateEntity parseJson(String json) throws Exception { CustomResult result = JsonUtil.fromJson(json, CustomResult.class); if (result != null) { return new UpdateEntity() .setHasUpdate(result.hasUpdate) .setIsIgnorable(result.isIgnorable) .setVersionCode(result.versionCode) .setVersionName(result.versionName) .setUpdateContent(result.updateLog) .setDownloadUrl(result.apkUrl) .setSize(result.apkSize); } return null; } } 3.7、自定义版本更新检查器+版本更新解析器+版本更新提示器实现IUpdateChecker接口即可实现检查器的自定义。实现IUpdateParser接口即可实现解析器的自定义。实现IUpdatePrompter接口即可实现提示器的自定义。XUpdate.newBuild(getActivity()) .updateUrl(mUpdateUrl3) .updateChecker(new DefaultUpdateChecker() { @Override public void onBeforeCheck() { super.onBeforeCheck(); CProgressDialogUtils.showProgressDialog(getActivity(), "查询中..."); } @Override public void onAfterCheck() { super.onAfterCheck(); CProgressDialogUtils.cancelProgressDialog(getActivity()); } }) .updateParser(new CustomUpdateParser()) .updatePrompter(new CustomUpdatePrompter(getActivity())) .update(); public class CustomUpdatePrompter implements IUpdatePrompter { private Context mContext; public CustomUpdatePrompter(Context context) { mContext = context; } @Override public void showPrompt(@NonNull UpdateEntity updateEntity, @NonNull IUpdateProxy updateProxy, @NonNull PromptEntity promptEntity) { showUpdatePrompt(updateEntity, updateProxy); } /** * 显示自定义提示 * * @param updateEntity * @param updateProxy */ private void showUpdatePrompt(final @NonNull UpdateEntity updateEntity, final @NonNull IUpdateProxy updateProxy) { String updateInfo = UpdateUtils.getDisplayUpdateInfo(mContext, updateEntity); new AlertDialog.Builder(mContext) .setTitle(String.format("是否升级到%s版本?", updateEntity.getVersionName())) .setMessage(updateInfo) .setPositiveButton("升级", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { updateProxy.startDownload(updateEntity, new OnFileDownloadListener() { @Override public void onStart() { HProgressDialogUtils.showHorizontalProgressDialog(mContext, "下载进度", false); } @Override public void onProgress(float progress, long total) { HProgressDialogUtils.setProgress(Math.round(progress * 100)); } @Override public boolean onCompleted(File file) { HProgressDialogUtils.cancel(); return true; } @Override public void onError(Throwable throwable) { HProgressDialogUtils.cancel(); } }); } }) .setNegativeButton("暂不升级", null) .setCancelable(false) .create() .show(); }3.8、只使用XUpdate的下载器功能进行apk的下载XUpdate.newBuild(getActivity()) .apkCacheDir(PathUtils.getExtDownloadsPath()) //设置下载缓存的根目录 .build() .download(mDownloadUrl, new OnFileDownloadListener() { //设置下载的地址和下载的监听 @Override public void onStart() { HProgressDialogUtils.showHorizontalProgressDialog(getContext(), "下载进度", false); } @Override public void onProgress(float progress, long total) { HProgressDialogUtils.setProgress(Math.round(progress * 100)); } @Override public boolean onCompleted(File file) { HProgressDialogUtils.cancel(); ToastUtils.toast("apk下载完毕,文件路径:" + file.getPath()); return false; } @Override public void onError(Throwable throwable) { HProgressDialogUtils.cancel(); } });3.9、只使用XUpdate的APK安装的功能_XUpdate.startInstallApk(getContext(), FileUtils.getFileByPath(PathUtils.getFilePathByUri(getContext(), data.getData()))); //填写文件所在的路径如果你的apk安装与众不同,你可以实现自己的apk安装器。你只需要实现OnInstallListener接口,并通过XUpdate.setOnInstallListener进行设置即可生效。混淆配置-keep class com.xuexiang.xupdate.entity.** { *; } //注意,如果你使用的是自定义Api解析器解析,还需要给你自定义Api实体配上混淆,如下是本demo中配置的自定义Api实体混淆规则: -keep class com.xuexiang.xupdatedemo.entity.** { *; } 相关链接XUpdate 文档XUpdate 管理服务XUpdate 后台管理系统XUpdate Flutter插件XUpdate React-Native插件Flutter版本更新弹窗组件特别感谢https://github.com/WVector/AppUpdate
2020年12月14日
1,695 阅读
0 评论
1 点赞
2020-12-13
Lint found errors in the project; aborting build导致打包停止错误解决
今天在打包时的时候出现了这个错导致打包不成功,具体原因可以从报错日志中看到是因为studio检测到了lint错误,从而停止了打包。解决办法是在app下的build.gradle 文件的android节点下添加lintOptions{ abortOnError false }即可,即在检测到lint错误时还接续进行打包。7:00:55 :app:lint FAILED 17:00:55 17:00:55 FAILURE: Build failed with an exception. 17:00:55 17:00:55 * What went wrong: 17:00:55 Execution failed for task ':app:lint'. 17:00:55 > Lint found errors in the project; aborting build. 17:00:55 17:00:55 Fix the issues identified by lint, or add the following to your build script to proceed with errors: 17:00:55 ... 17:00:55 android { 17:00:55 lintOptions { 17:00:55 abortOnError false 17:00:55 } 17:00:55 } 17:00:55 ... 17:00:55 17:00:55 The first 3 errors (out of 6) were: 17:00:55 /root/.gradle/caches/modules-2/files-2.1/com.alibaba/fastjson/1.2.35/d0b54f814af4652f0893d560d6a785917abdeae1/fastjson-1.2.35.jar: Error: Invalid package reference in library; not included in Android: java.awt. Referenced from com.alibaba.fastjson.serializer.AwtCodec. [InvalidPackage] 17:00:55 /root/.gradle/caches/modules-2/files-2.1/com.alibaba/fastjson/1.2.35/d0b54f814af4652f0893d560d6a785917abdeae1/fastjson-1.2.35.jar: Error: Invalid package reference in library; not included in Android: javax.servlet.http. Referenced from com.alibaba.fastjson.support.spring.FastJsonJsonView. [InvalidPackage] 17:00:55 /root/.gradle/caches/modules-2/files-2.1/com.alibaba/fastjson/1.2.35/d0b54f814af4652f0893d560d6a785917abdeae1/fastjson-1.2.35.jar: Error: Invalid package reference in library; not included in Android: javax.servlet. Referenced from com.alibaba.fastjson.support.spring.FastJsonJsonView. [InvalidPackage]
2020年12月13日
2,592 阅读
0 评论
3 点赞
2020-12-10
非常简单的Android Studio中第三方源码导入
Android开发时,我们经常需要导入第三方的源码。之前看看其他人分享的,感觉都看的稀里糊涂的,作为一个Android开发新人一脸懵逼。这里给大家介绍一种非常简单的导入方法。这里以一个动态权限的源码导入为例子进行说明。github地址:https://github.com/forJrking/HeiPermission下载源码这个不多介绍,去上面地址下载就行了。解压代码解压代码,我们会得到permlib文件夹,如下拷贝代码将permlib整个文件夹拷贝到我们的工程中修改settings.gradle找到我们工程的settings.gradle文件,修改如下添加依赖找到我们模块的build.gradle。修改项目结构打开项目结构,修改模块的jdk版本最后引入完成后Android studio会自动构建成功,如果有报错,修改错误即可。
2020年12月10日
1,276 阅读
0 评论
1 点赞
1
2