开发)
通过与华为云平台进⾏数据对接及联动控制,为此开发智慧路灯APP控制系统。
1、系统总体描述
本系统共分为九个模块:系统导航、⽤户登录、扫码绑定、设备定位、设备状态、历史查询、设备控制、画像分析和系统设置。每个模块对应其各⾃的功能,通过设备的定位、设备的实时状态及设备控制能够全⽅位监控路灯的耗能量及使⽤情况。画像分析也可对某地⽅或某⽤户进⾏⼤数据AI分析得到监测数据,并且能够实时向⽤户推送⽤电情况,并为其⽤户进⾏合理的⽤电安排及⽅案。
2、使⽤技术
2.1、通⽤技术
系统总体使⽤java语⾔进⾏开发;
在界⾯设计及展⽰部分使⽤HTML搭配CSS技术使其界⾯美观⼤⽅;框架设计使⽤MVP模式进⾏设计使其系统结构清晰明了;
数据对接使⽤HTTP和OkHttp3协议,⼤⼤降低数据处理难度;且提⾼了数据的完整性和实时性。
2.2、核⼼技术
登录界⾯使⽤视屏背景技术将登录界⾯进⾏⾼度美化。在设备定位模块中使⽤第三⽅⾼德地图SDK进⾏开发;在云平台对接时使⽤华为云平台相关模块接⼝进⾏开发;使⽤Clendar相关类进⾏⽇期选择设计;
使⽤Zxing⼆维码扫描分析技术进⾏扫码分析;使⽤Echart技术进⾏数据实时显⽰图表分析;
在画像分析模块使⽤AI⼤数据分析获取数据实例。
2.3、技术亮点
对第三⽅技术的合理运⽤;对MVP开发框架的组合设计;对API接⼝的清晰掌握;
对各种相关⼯具类的开发及调⽤;结合⼤数据AI分析进⾏功能设计。
3、开发软件
3.1、开发软件
系统环境:Windows 10
开发环境:Android Studio 3.0,JDK 8.0运⾏环境:Android 5.0级以上
3.2、测试软件
接⼝测试软件:Postman 6.5
3.3、打包发布软件
版本控制软件:Git
打包发布软件:Android Studio (Generate Signed APK)
软件签名:iot_project.jks
4、功能概述
4.1、系统导航
⾸次进⼊APP当进⼊导航界⾯,导航界⾯中介绍APP的Logo、简单描述、路灯模型、路灯功能分类及路灯运⾏⽅式。效果如下所⽰:
4.2、⽤户登录
⽤户登录界⾯使⽤视屏作为页⾯背景,通过输⼊⽤户名及密码进⾏系统登录。系统的⽤户名及密码在系统后台统⼀进⾏注册。效果如下所⽰:
4.3、扫码绑定
⽤户登录成功后将⾃动跳转⾄扫码界⾯,跳转界⾯后会对该移动设备进⾏权限访问,⽤户需要同意所有权限才能正常使⽤该系统。授权后进⾏⼆维码扫描。此时需要对路灯上的⼆维码进⾏扫码,通过扫码得到该路灯的设备信息,从⽽在主界⾯中可查看该路灯的其他信息。 效果如下所⽰:
4.4、设备定位
此模块中将对该扫描设备进⾏设备定位,观察其设备所在的具体位置,并能够查看当前地⽅的天⽓环境。此处的设计也是为后来的管理⽅便,对每⼀个路灯设备能够全⽅位的进⾏查看。效果如下所⽰:
4.5、设备状态
此模块将对所在设备的所有信息进⾏实时查看,有电压、电流、功率、功率因⼦、总耗电量、光照度、路灯开光状态及路灯耗能所产⽣的⼆氧化碳量。 效果如下所⽰:
4.6、历史查询
此模块是对该路灯所有数据的历史查询,通过对历史数据的查询可分析出该设备在本周、本⽉及本年的所有⽤电量情况。这样就能够合理的对路灯⽤电量进⾏管理。效果如下所⽰:
4.7、设备控制
此模块是对路灯的远程控制,共分为三个模式分别为:终端联控模式、分段定时模式及⾃动调光模式。三种模式分别对应三种不同的路灯控制,可远程也可⾃动,充分达到了⽤电量的控制。效果如下所⽰:
4.8、画像分析
此模块涉及了⼤数据AI分析功能,将分析的数据下发⾄该系统,系统对其数据进⾏图⽂的可视化展⽰,清晰的可以查看到该使⽤者⽇常⽤电情况及地⽅⽤电情况。(由于数据集较少,建⽴的模型是我本⼈2019年7⽉份路灯节点使⽤状况) 效果如下所⽰:
4.9、系统设置
系统设置功能共分为以下⼏点:系统设置、修改密码、关于我们、系统更新及退出登录。效果如下所⽰:
5、核⼼代码
5.1、数据获取核⼼代码
public class NetConnectHeaderDataJSON {
public static final String REQUEST_TYPE_GET = \"GET\"; public static final String REQUEST_TYPE_POST = \"POST\"; public static final String REQUEST_TYPE_PUT = \"PUT\"; /**
* 请求URL并返回内容 *
* @param method * ⽅式get post * @param url * 地址 * @param param * 参数 * @return json */
public static String request(Context context, String method, String url, String app_id, String token, List if (method.equals(REQUEST_TYPE_GET)){ String ps =\"\"; if (param != null) { List String key = param.get(i).getName(); String value =param.get(i) .getValue(); ps+=key+\"=\"+value; } ps = URLEncodedUtils.format(param2, HTTP.UTF_8); // 通过url创建对象 if (url.indexOf(\"?\") > 0) { url += \"&\" + ps; } else { url += \"?\" + ps; } } SSLSocketFactory ssl=DataManager.GETSSLinitHttpClientBook(context); HttpClient httpClient = new DefaultHttpClient(); if (ssl != null) { Scheme sch = new Scheme(\"https\ httpClient.getConnectionManager().getSchemeRegistry().register(sch); } HttpGet request = new HttpGet(url); request.setHeader(\"app_key\ request.setHeader(\"Authorization\ request.setHeader(\"Content-Type\// request.setEntity(new UrlEncodedFormEntity(param)); // 发起请求,获取回应,⾃封装接⼝,详见附录 response = httpClient.execute(request); HttpEntity httpEntity = response.getEntity(); // 得到⼀些数据 // 通过EntityUtils并指定编码⽅式取到返回的数据 StatusLine statusLine = response.getStatusLine(); statusLine.getProtocolVersion(); int statusCode = statusLine.getStatusCode(); if (statusCode == 200) { result_https = (EntityUtils.toString(httpEntity, \"utf-8\")); }else{ result_https=\"\"+statusCode; } }else if(method.equals(REQUEST_TYPE_POST)){ SSLSocketFactory ssl=DataManager.GETSSLinitHttpClientBook(context); HttpClient httpClient = new DefaultHttpClient(); if (ssl != null) { Scheme sch = new Scheme(\"https\ httpClient.getConnectionManager().getSchemeRegistry().register(sch); } HttpPost request = new HttpPost(url); if (param != null) { String JsonData=\"\"; String startdata=\"{\"; String enddata=\ for (int i = 0; i < param.size(); i++) { String key = param.get(i).getName(); String values = param.get(i).getValue(); // param2.add(new BasicNameValuePair(key, values)); if (i>0) JsonData+=\ if (key.equals(\"timeout\")) JsonData+=\"\\\"\"+key+\"\\\":\"+values; else JsonData+=\"\\\"\"+key+\"\\\":\\\"\"+values+\"\\\"\"; } //传⼊的是json格式的数据 JsonData=startdata+JsonData+enddata; request.setEntity(new StringEntity(JsonData, HTTP.UTF_8)); } request.setHeader(\"app_key\ request.setHeader(\"Authorization\ request.setHeader(\"Content-Type\ response = httpClient.execute(request); HttpEntity httpEntity = response.getEntity(); // 通过EntityUtils并指定编码⽅式取到返回的数据 StatusLine statusLine = response.getStatusLine(); int statusCode = statusLine.getStatusCode(); if (statusCode == 200) { result_https = (EntityUtils.toString(httpEntity, \"utf-8\")); }else{ result_https=\"\"+statusCode; } }else if(method.equals(REQUEST_TYPE_PUT)){ SSLSocketFactory ssl=DataManager.GETSSLinitHttpClientBook(context); HttpClient httpClient = new DefaultHttpClient(); if (ssl != null) { Scheme sch = new Scheme(\"https\ httpClient.getConnectionManager().getSchemeRegistry().register(sch); } HttpPut request = new HttpPut(url); if (param != null) { String JsonData=\"\"; String startdata=\"{\"; String enddata=\ for (int i = 0; i < param.size(); i++) { String key = param.get(i).getName(); String values = param.get(i).getValue(); if (i>0) JsonData+=\ if (key.equals(\"timeout\")) JsonData+=\"\\\"\"+key+\"\\\":\"+values; else JsonData+=\"\\\"\"+key+\"\\\":\\\"\"+values+\"\\\"\"; } //传⼊的是json格式的数据 JsonData=startdata+JsonData+enddata; request.setEntity(new StringEntity(JsonData, HTTP.UTF_8)); } // request.setHeader(\"app_key\ request.setHeader(\"Authorization\ request.setHeader(\"Content-Type\ response = httpClient.execute(request); HttpEntity httpEntity = response.getEntity(); // 通过EntityUtils并指定编码⽅式取到返回的数据 StatusLine statusLine = response.getStatusLine(); int statusCode = statusLine.getStatusCode(); if (statusCode == 204) { result_https = (EntityUtils.toString(httpEntity, \"utf-8\")); }else{ result_https=\"\"+statusCode; } } else{ Log.i(\"method==\ } return result_https; }} 5.2、第三⽅SDK调⽤核⼼代码 /** * ⽅法必须重写 */ @Override public void onResume() { super.onResume(); mapView.onResume();}/** * ⽅法必须重写 */ @Override public void onPause() { super.onPause(); mapView.onPause();}/** * ⽅法必须重写 */ @Override public void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); mapView.onSaveInstanceState(outState);}/** * ⽅法必须重写 */ @Override public void onDestroy() { super.onDestroy(); mapView.onDestroy(); if (mTimerTask != null) { mTimerTask.cancel(); mTimerTask = null; } try { mTimer.cancel(); } catch (Throwable e) { e.printStackTrace(); } deactivate();} @Override public void onMapClick(LatLng latLng) { } @Override public void onMapLoaded() { aMap.moveCamera(CameraUpdateFactory.zoomTo(zoomLevel));} @Override public void activate(OnLocationChangedListener onLocationChangedListener) { mListener = onLocationChangedListener; startlocation();} @Override public void deactivate() { mListener = null; if (mLocationClient != null) { mLocationClient.stopLocation(); mLocationClient.onDestroy(); } mLocationClient = null;}/** * 开始定位。 */ private void startlocation() { if (mLocationClient == null) { mLocationClient = new AMapLocationClient(getActivity()); mLocationOption = new AMapLocationClientOption(); // 设置定位监听 mLocationClient.setLocationListener(this); // 设置为⾼精度定位模式 mLocationOption.setLocationMode(AMapLocationClientOption.AMapLocationMode.Hight_Accuracy); //设置为单次定位 mLocationOption.setNeedAddress(true); mLocationOption.setOnceLocation(true); // 设置定位参数 mLocationClient.setLocationOption(mLocationOption); mLocationClient.startLocation(); } else { mLocationClient.startLocation(); }} @Override public void onLocationChanged(AMapLocation aMapLocation) { if (mListener != null && aMapLocation != null) { if (mTimerTask != null) { mTimerTask.cancel(); mTimerTask = null; } if (aMapLocation != null && aMapLocation.getErrorCode() == 0) { LatLng mylocation = new LatLng(Absoult.Latitude, Absoult.Longitude); changeCamera(CameraUpdateFactory.newLatLngZoom(mylocation,zoomLevel),null); SimpleDateFormat df = new SimpleDateFormat(\"yyyy-MM-dd HH:mm:ss\"); Date date = new Date(aMapLocation.getTime()); mTextViewCity.setText(aMapLocation.getCountry()+aMapLocation.getProvince()+aMapLocation.getCity()); mTextViewAddress.setText(aMapLocation.getAddress()); cityName = aMapLocation.getCity().substring(0,aMapLocation.getCity().length()-1); initWaetherData(cityName); aMapLocation.getAddress();//地址,如果option中设置isNeedAddress为false,则没有此结果,⽹络定位结果中会有地址信息,GPS定位不返回地址信息。 aMapLocation.getCountry();//国家信息 aMapLocation.getProvince();//省信息 aMapLocation.getCity();//城市信息 aMapLocation.getDistrict();//城区信息 aMapLocation.getStreet();//街道信息 aMapLocation.getStreetNum();//街道门牌号信息 aMapLocation.getCityCode();//城市编码 aMapLocation.getAdCode();//地区编码 df.format(date);//定位时间 if (locMarker==null){ addLocationMarker(mylocation); }else { locMarker.setPosition(mylocation); } if (ac!=null){ ac.setCenter(mylocation); } if (c!=null){ c.setCenter(mylocation); } if (d!=null){ d.setCenter(mylocation); } } else { String errText = \"定位失败,\" + aMapLocation.getErrorCode() + \": \" + aMapLocation.getErrorInfo(); Log.e(\"AmapErr\ } }}/** * 添加坐标点,这⾥可以添加任意坐标点位置 * @param mylocation */ private void addLocationMarker(LatLng mylocation) { float accuracy = (float) ((mylocation.longitude/mylocation.latitude )); if (locMarker == null) { locMarker = addMarker(mylocation); if (ac==null){ ac = aMap.addCircle(new CircleOptions().center(mylocation) .fillColor(Color.argb(0, 98 ,1, 255)).radius(accuracy) .strokeColor(Color.argb(0, 98, 198, 255)).strokeWidth(0)); } if (c==null){ c = aMap.addCircle(new CircleOptions().center(mylocation) .fillColor(Color.argb(0, 98, 198, 255)) .radius(accuracy).strokeColor(Color.argb(0,98, 198, 255)) .strokeWidth(0)); } if (d==null){ d = aMap.addCircle(new CircleOptions().center(mylocation) .fillColor(Color.argb(0, 98, 198, 255)) .radius(accuracy).strokeColor(Color.argb(0,98, 198, 255)) .strokeWidth(0)); } } else { locMarker.setPosition(mylocation); ac.setCenter(mylocation); ac.setRadius(accuracy); c.setCenter(mylocation); c.setRadius(accuracy); d.setCenter(mylocation); d.setRadius(accuracy); } handle.postDelayed(rb,0); handle1.postDelayed(rb1,800); handle2.postDelayed(rb2,1600);}/** * 位置波纹扩散动画 * @param ac */ private void Scalecircle1(final Circle ac) { ValueAnimator vm = ValueAnimator.ofFloat(0,(float)ac.getRadius()); vm.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { float curent = (float) animation.getAnimatedValue(); ac.setRadius(curent); aMap.invalidate(); } }); ValueAnimator vm1 = ValueAnimator.ofInt(160,0); vm1.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { int color = (int) animation.getAnimatedValue(); ac.setFillColor(Color.argb(color, 98, 198, 255)); aMap.invalidate(); } }); vm.setRepeatCount(Integer.MAX_VALUE); vm.setRepeatMode(ValueAnimator.RESTART); vm1.setRepeatCount(Integer.MAX_VALUE); vm1.setRepeatMode(ValueAnimator.RESTART); AnimatorSet set = new AnimatorSet(); set.play(vm).with(vm1); set.setDuration(2500); set.setInterpolator(interpolator1); set.start();} private final Interpolator interpolator1 = new LinearInterpolator();Runnable rb = new Runnable() { @Override public void run() { Scalecircle1(ac); }}; Handler handle =new Handler();Runnable rb1 = new Runnable() { @Override public void run() { Scalecircle1(c); }}; Handler handle1 =new Handler();Runnable rb2 = new Runnable() { @Override public void run() { Scalecircle1(d); }}; Handler handle2 =new Handler(); 5.3、图表数据展⽰核⼼代码 public class ChartService { private GraphicalView mGraphicalView; private XYMultipleSeriesDataset multipleSeriesDataset;// 数据集容器 private XYMultipleSeriesRenderer multipleSeriesRenderer;// 渲染器容器 private XYSeries mSeries;// 单条曲线数据集 private XYSeriesRenderer mRenderer;// 单条曲线渲染器 private Context context; private Double xx ; int aa = 100; public ChartService(Context context) { this.context = context; } /** * 获取图表 * * @return */ public GraphicalView getGraphicalView() { mGraphicalView = ChartFactory.getCubeLineChartView(context, multipleSeriesDataset, multipleSeriesRenderer, 0.1f); return mGraphicalView; } /** * 获取数据集,及xy坐标的集合 * * @param curveTitle */ public void setXYMultipleSeriesDataset(String curveTitle) { multipleSeriesDataset = new XYMultipleSeriesDataset(); mSeries = new XYSeries(curveTitle); multipleSeriesDataset.addSeries(mSeries); } /** * 获取渲染器 * * @param maxX * x轴最⼤值 * @param maxY * y轴最⼤值 * @param chartTitle * 曲线的标题 * @param xTitle * x轴标题 * @param yTitle * y轴标题 * @param axeColor * 坐标轴颜⾊ * @param labelColor * 标题颜⾊ * @param curveColor * 曲线颜⾊ * @param gridColor * ⽹格颜⾊ */ public void setXYMultipleSeriesRenderer(double maxX, double maxY, String chartTitle, String xTitle, String yTitle, int axeColor, int labelColor, int curveColor, int gridColor) { multipleSeriesRenderer = new XYMultipleSeriesRenderer(); if (chartTitle != null) { multipleSeriesRenderer.setChartTitle(chartTitle); } multipleSeriesRenderer.setXTitle(xTitle); multipleSeriesRenderer.setYTitle(yTitle); multipleSeriesRenderer.setRange(new double[] { 0, maxX, 0, maxY });//xy轴的范围 multipleSeriesRenderer.setLabelsColor(labelColor); multipleSeriesRenderer.setXLabels(5); multipleSeriesRenderer.setYLabels(10); multipleSeriesRenderer.setXLabelsAlign(Align.RIGHT); multipleSeriesRenderer.setYLabelsAlign(Align.RIGHT); multipleSeriesRenderer.setAxisTitleTextSize(35); multipleSeriesRenderer.setChartTitleTextSize(35); multipleSeriesRenderer.setLabelsTextSize(35); multipleSeriesRenderer.setLegendTextSize(35); multipleSeriesRenderer.setPointSize(5f);//曲线描点尺⼨ multipleSeriesRenderer.setFitLegend(true); multipleSeriesRenderer.setMargins(new int[] { 80, 80, 80, 80 }); multipleSeriesRenderer.setShowGrid(true); multipleSeriesRenderer.setPanEnabled(false, false);//允许X轴可拉动 multipleSeriesRenderer.setZoomEnabled(false, false); multipleSeriesRenderer.setAxesColor(axeColor); multipleSeriesRenderer.setGridColor(gridColor); multipleSeriesRenderer.setBackgroundColor(Color.WHITE);//背景⾊ multipleSeriesRenderer.setMarginsColor(Color.WHITE);//边距背景⾊,默认背景⾊为⿊⾊,这⾥修改为⽩⾊ mRenderer = new XYSeriesRenderer(); mRenderer.setLineWidth(3f); mRenderer.setColor(curveColor); mRenderer.setPointStrokeWidth(5f); mRenderer.setPointStyle(PointStyle.CIRCLE);//描点风格,可以为圆点,⽅形点等等 multipleSeriesRenderer.addSeriesRenderer(mRenderer); } /** * 根据新加的数据,更新曲线,只能运⾏在主线程 * * @param x * 新加点的x坐标 * @param y * 新加点的y坐标 */ public void updateChart(double x, double y) { mSeries.add(x, y); if (x>aa) { aa += 1; multipleSeriesRenderer.setXAxisMax(aa);// 设置X最⼤值 multipleSeriesRenderer.setXAxisMin(aa - 100);// 设置X最⼩值 } mGraphicalView.repaint();//此处也可以调⽤invalidate() } /** * 添加新的数据,多组,更新曲线,只能运⾏在主线程 * @param xList * @param yList */ public void updateChart(List mSeries.add(xx, yList.get(i)); } mGraphicalView.repaint();//此处也可以调⽤invalidate() }} 5.4、⽇历数据查询核⼼代码 public abstract class NCalendar extends FrameLayout implements NestedScrollingParent, OnCalendarStateChangedListener, OnDateChangedListener, OnMonthAnimatorListener { protected WeekCalendar weekCalendar; protected MonthCalendar monthCalendar; protected int weekHeight;//周⽇历的⾼度 protected int monthHeight;//⽉⽇历的⾼度,是⽇历整个的⾼ protected int childLayoutLayoutTop;//onLayout中,定位的⾼度, protected int STATE;//默认⽉ private int lastSate;//防⽌状态监听重复回调 private OnCalendarChangedListener onCalendarChangedListener; //NCalendar内部包含的直接⼦view,直接⼦view并不⼀定是NestScrillChild protected ChildLayout childLayout; protected Rect monthRect;//⽉⽇历⼤⼩的矩形 protected Rect weekRect;//周⽇历⼤⼩的矩形 ,⽤于判断点击事件是否在⽇历的范 private boolean isWeekHold;//是否需要周状态定住 public NCalendar(@NonNull Context context) { this(context, null); } public NCalendar(@NonNull Context context, @Nullable AttributeSet attrs) { this(context, attrs, 0); } public NCalendar(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); setMotionEventSplittingEnabled(false); Attrs attrss = AttrsUtil.getAttrs(context, attrs); int duration = attrss.duration; monthHeight = attrss.monthCalendarHeight; STATE = attrss.defaultCalendar; weekHeight = monthHeight / 5; isWeekHold = attrss.isWeekHold; weekCalendar = new WeekCalendar(context, attrss); monthCalendar = new MonthCalendar(context, attrss, duration, this); childLayout = new ChildLayout(getContext(), attrs, monthHeight, duration, this); monthCalendar.setOnDateChangedListener(this); weekCalendar.setOnDateChangedListener(this); childLayout.setBackgroundColor(attrss.bgChildColor); setCalenadrState(STATE); childLayoutLayoutTop = STATE == Attrs.WEEK ? weekHeight : monthHeight; post(new Runnable() { @Override public void run() { monthRect = new Rect(0, 0, monthCalendar.getWidth(), monthCalendar.getHeight()); weekRect = new Rect(0, 0, weekCalendar.getWidth(), weekCalendar.getHeight()); monthCalendar.setY(STATE == Attrs.MONTH ? 0 : getMonthYOnWeekState()); childLayout.setY(STATE == Attrs.MONTH ? monthHeight : weekHeight); } }); } /** * 根据ChildLayout的⾃动滑动结束的状态来设置⽉周⽇历的状态 * 依据ChildLayout的状态来设置⽇历的状态 * * @param isMonthState */ @Override public void onCalendarStateChanged(boolean isMonthState) { if (isMonthState) { setCalenadrState(Attrs.MONTH); } else { setCalenadrState(Attrs.WEEK); } } /** * xml⽂件加载结束,添加⽉,周⽇历和child到NCalendar中 */ @Override protected void onFinishInflate() { super.onFinishInflate(); if (getChildCount() != 1) { throw new RuntimeException(\"NCalendar中的只能有⼀个直接⼦view\"); } childLayout.addView(getChildAt(0), new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); addView(monthCalendar, new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, monthHeight)); addView(weekCalendar, new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, weekHeight)); addView(childLayout, new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); ViewGroup.LayoutParams childLayoutLayoutParams = childLayout.getLayoutParams(); childLayoutLayoutParams.height = getMeasuredHeight() - weekHeight; //需要再调⼀次⽗类的⽅法?真机不调⽤⾸次⾼度不对,为何? super.onMeasure(widthMeasureSpec, heightMeasureSpec); } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { //super.onLayout(changed, l, t, r, b); //调⽤⽗类的该⽅法会造成 快速滑动⽉⽇历同时快速上滑recyclerview造成⽉⽇历的残影 int measuredWidth = getMeasuredWidth(); weekCalendar.layout(0, 0, measuredWidth, weekHeight); monthCalendar.layout(0, 0, measuredWidth, monthHeight); childLayout.layout(0, childLayoutLayoutTop, measuredWidth, childLayout.getMeasuredHeight() + childLayoutLayoutTop); } /** * 根据条件设置⽇历的⽉周状态,并回调状态变化 * * @param state */ private void setCalenadrState(int state) { if (state == Attrs.WEEK) { STATE = Attrs.WEEK; weekCalendar.setVisibility(VISIBLE); } else { STATE = Attrs.MONTH; weekCalendar.setVisibility(INVISIBLE); } if (onCalendarChangedListener != null && lastSate != state) { onCalendarChangedListener.onCalendarStateChanged(STATE == Attrs.MONTH); } lastSate = state; } /** * ⾃动滑动到适当的位置 */ private void autoScroll() { float childLayoutY = childLayout.getY(); if (STATE == Attrs.MONTH && monthHeight - childLayoutY < weekHeight) { onAutoToMonthState(); } else if (STATE == Attrs.MONTH && monthHeight - childLayoutY >= weekHeight) { onAutoToWeekState(); } else if (STATE == Attrs.WEEK && childLayoutY < weekHeight * 2) { onAutoToWeekState(); } else if (STATE == Attrs.WEEK && childLayoutY >= weekHeight * 2) { onAutoToMonthState(); } } /** * ⽉⽇历和周⽇历的⽇期变化回调,每次⽇期变化都会回调,⽤于不同状态下,设置另⼀个⽇历的⽇期 * * @param baseCalendar ⽇历本⾝ * @param localDate 当前选中的时间 * @param isDraw 是否绘制 此处选择都绘制,默认不选中,不适⽤鱼⽉周切换 */ @Override public void onDateChanged(BaseCalendar baseCalendar, LocalDate localDate, boolean isDraw) { if (baseCalendar instanceof MonthCalendar && STATE == Attrs.MONTH) { //⽉⽇历变化,改变周的选中 weekCalendar.jumpDate(localDate, true); if (onCalendarChangedListener != null) { onCalendarChangedListener.onCalendarDateChanged(Util.getNDate(localDate)); } } else if (baseCalendar instanceof WeekCalendar && STATE == Attrs.WEEK) { //周⽇历变化,改变⽉的选中 monthCalendar.jumpDate(localDate, true); post(new Runnable() { @Override public void run() { //此时需要根据⽉⽇历的选中⽇期调整Y值 // post是因为在前⾯得到当前view是再post中完成,如果不这样直接获取位置信息,会出现⽼的数据,不能获取正确的数据 monthCalendar.setY(getMonthYOnWeekState()); } }); if (onCalendarChangedListener != null) { onCalendarChangedListener.onCalendarDateChanged(Util.getNDate(localDate)); } } } @Override public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) { return true; } @Override public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) { //跟随⼿势滑动 gestureMove(dy, consumed); } @Override public boolean onNestedPreFling(View target, float velocityX, float velocityY) { //只有都在都在周状态下,才允许⼦View Fling滑动 return !(childLayout.isWeekState() && monthCalendar.isWeekState()); } @Override public void onStopNestedScroll(View target) { //该⽅法⼿指抬起的时候回调,此时根据此刻的位置,⾃动滑动到相应的状态, //如果已经在对应的位置上,则不执⾏动画, if (monthCalendar.isMonthState() && childLayout.isMonthState() && STATE == Attrs.WEEK) { setCalenadrState(Attrs.MONTH); } else if (monthCalendar.isWeekState() && childLayout.isWeekState() && STATE == Attrs.MONTH) { setCalenadrState(Attrs.WEEK); } else if (!childLayout.isMonthState() && !childLayout.isWeekState()) { //不是周状态也不是⽉状态时,⾃动滑动 autoScroll(); } } /** * ⼿势滑动的逻辑,做了简单处理,2种状态,都以ChildLayout滑动的状态判断 * 1、向上滑动未到周状态 * 2、向下滑动未到⽉状态 * * @param dy * @param consumed */ protected void gestureMove(int dy, int[] consumed) { float monthCalendarY = monthCalendar.getY(); float childLayoutY = childLayout.getY(); if (dy > 0 && !childLayout.isWeekState()) { monthCalendar.setY(-getGestureMonthUpOffset(dy) + monthCalendarY); childLayout.setY(-getGestureChildUpOffset(dy) + childLayoutY); if (consumed != null) consumed[1] = dy; } else if (dy < 0 && isWeekHold && childLayout.isWeekState()) { //不操作, } else if (dy < 0 && !childLayout.isMonthState() && !childLayout.canScrollVertically(-1)) { monthCalendar.setY(getGestureMonthDownOffset(dy) + monthCalendarY); childLayout.setY(getGestureChildDownOffset(dy) + childLayoutY); if (consumed != null) consumed[1] = dy; } onSetWeekVisible(dy); } /** * ⽉⽇历执⾏⾃动滑动动画的回调 * ⽤来控制周⽇历的显⽰还是隐藏 * * @param offset */ @Override public void onMonthAnimatorChanged(int offset) { onSetWeekVisible(offset); } private int dowmY; private int downX; private int lastY;//上次的y private int verticalY = 50;//竖直⽅向上滑动的临界值,⼤于这个值认为是竖直滑动 private boolean isFirstScroll = true; //第⼀次⼿势滑动,因为第⼀次滑动的偏移量⼤于verticalY,会出现猛的⼀划,这⾥只对第⼀次滑动做处理 @Override public boolean onInterceptTouchEvent(MotionEvent ev) { switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: dowmY = (int) ev.getY(); downX = (int) ev.getX(); lastY = dowmY; break; case MotionEvent.ACTION_MOVE: int y = (int) ev.getY(); int absY = Math.abs(dowmY - y); boolean inCalendar = isInCalendar(downX, dowmY); if (absY > verticalY && inCalendar) { //onInterceptTouchEvent返回true,触摸事件交给当前的onTouchEvent处理 return true; } break; } return super.onInterceptTouchEvent(ev); } @Override public boolean onTouchEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_MOVE: int y = (int) event.getY(); int dy = lastY - y; if (isFirstScroll) { // 防⽌第⼀次的偏移量过⼤ if (dy > verticalY) { dy = dy - verticalY; } else if (dy < -verticalY) { dy = dy + verticalY; } isFirstScroll = false; } // 跟随⼿势滑动 gestureMove(dy, null); lastY = y; break; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: isFirstScroll = true; autoScroll(); break; } return true; } /** * 点击事件是否在⽇历的范围内 * * @param x * @param y * @return */ private boolean isInCalendar(int x, int y) { if (STATE == Attrs.MONTH) { return monthRect.contains(x, y); } else { return weekRect.contains(x, y); } } /** * 滑动过界处理 ,如果⼤于最⼤距离就返回最⼤距离 * * @param offset 当前滑动的距离 * @param maxOffset 当前滑动的最⼤距离 * @return */ protected float getOffset(float offset, float maxOffset) { if (offset > maxOffset) { return maxOffset; } return offset; } /** * ⾃动回到⽉的状态 包括⽉⽇历和chilayout */ protected abstract void onAutoToMonthState(); /** * ⾃动回到周的状态 包括⽉⽇历和chilayout */ protected abstract void onAutoToWeekState(); /** * 设置weekCalendar的显⽰隐藏,该⽅法会在⼿势滑动和⾃动滑动的的时候⼀直回调 */ protected abstract void onSetWeekVisible(int dy); /** * 周状态下 ⽉⽇历的getY 是个负值 * ⽤于在 周状态下⽇期改变设置正确的y值 * * @return */ protected abstract float getMonthYOnWeekState(); /** * ⽉⽇历根据⼿势向上移动的距离 * * @param dy 当前滑动的距离 dy>0向上滑动,dy<0向下滑动 * @return 根据不同⽇历的交互,计算不同的滑动值 */ protected abstract float getGestureMonthUpOffset(int dy); /** * Child根据⼿势向上移动的距离 * * @param dy 当前滑动的距离 dy>0向上滑动,dy<0向下滑动 * @return 根据不同⽇历的交互,计算不同的滑动值 */ protected abstract float getGestureChildUpOffset(int dy); /** * ⽉⽇历根据⼿势向下移动的距离 * * @param dy 当前滑动的距离 dy>0向上滑动,dy<0向下滑动 * @return 根据不同⽇历的交互,计算不同的滑动值 */ protected abstract float getGestureMonthDownOffset(int dy); /** * Child根据⼿势向下移动的距离 * * @param dy 当前滑动的距离 dy>0向上滑动,dy<0向下滑动 * @return 根据不同⽇历的交互,计算不同的滑动值 */ protected abstract float getGestureChildDownOffset(int dy); /** * 跳转⽇期 * * @param formatDate */ public void jumpDate( String formatDate) { if (STATE == Attrs.MONTH) { monthCalendar.jumpDate(formatDate); } else { weekCalendar.jumpDate(formatDate); } } /** * ⽇历初始化的⽇期 * @param formatDate */ public void setInitializeDate(String formatDate) { monthCalendar.setInitializeDate(formatDate); weekCalendar.setInitializeDate(formatDate); } /** * 回到今天 */ public void toToday() { if (STATE == Attrs.MONTH) { monthCalendar.toToday(); } else { weekCalendar.toToday(); } } /** * ⾃动滑动到周视图 */ public void toWeek() { if (STATE == Attrs.MONTH) { onAutoToWeekState(); } } /** * ⾃动滑动到⽉视图 */ public void toMonth() { if (STATE == Attrs.WEEK) { onAutoToMonthState(); } } /** * 设置⼩圆点 * * @param pointList */ public void setPointList(List * 获取当前⽇历的状态 * Attrs.MONTH==⽉视图 Attrs.WEEK==周视图 * * @return */ public int getState() { return STATE; } /** * 下⼀页 */ public void toNextPager() { if (STATE == Attrs.MONTH) { monthCalendar.toNextPager(); } else { weekCalendar.toNextPager(); } } /** * 上⼀页 */ public void toLastPager() { if (STATE == Attrs.MONTH) { monthCalendar.toLastPager(); } else { weekCalendar.toLastPager(); } } /** * 设置⽇期区间 * * @param startFormatDate * @param endFormatDate */ public void setDateInterval(String startFormatDate, String endFormatDate) { monthCalendar.setDateInterval(startFormatDate, endFormatDate); weekCalendar.setDateInterval(startFormatDate, endFormatDate); } /** * ⽇期、状态回调 * * @param onCalendarChangedListener */ public void setOnCalendarChangedListener(OnCalendarChangedListener onCalendarChangedListener) { this.onCalendarChangedListener = onCalendarChangedListener; } /** * 点击不可⽤的⽇期回调 * * @param onClickDisableDateListener */ public void setOnClickDisableDateListener(OnClickDisableDateListener onClickDisableDateListener) { monthCalendar.setOnClickDisableDateListener(onClickDisableDateListener); weekCalendar.setOnClickDisableDateListener(onClickDisableDateListener); }} 作者:华为云特约供稿开发者
因篇幅问题不能全部显示,请点此查看更多更全内容