ios 8 的时间滚轮控件实现了扁平化,带来很好用户体验,android没有现成控件,小弟不才,数学与算法知识不过关,顾十分苦恼,幸好在github上找到sai大神实现代码,甚为欣喜,顾把学习这个控件点滴记录下来,分享给大家。项目原地址https://github.com/saiwu-bigkoo/Android-PickerView。

  ios 8 滚轮的效果:

  而sai大神控件的效果:

  哎,妈呀是不是效果95%相识啊。

  好了,废话少说,谈谈我从这个控件中收获的心得。

  首先,我们要高瞻远瞩看一下他的整体架构图,让我们对这个控件有一个整体感觉。

  通过这一个简单架构图,我们可以清晰看出:

  这虽然是一个很小控件,但是麻雀虽小五脏俱全,采用典型的mvc模式将页面展示,逻辑控制,数据加载实现了有效的分离,接下来,我就浅尝辄止将这几个类分析一下吧。

  万事开头难,我们当然是从最容易的开始,看看wheeladapter吧。

  wheeladapter负责将数据填充进来,这是一个经典adapter模式,对于adapter的作用于好处,我想没有必要在这里赘述。我们看看它的全貌吧:

public interface WheelAdapter<T> {
/**
* Gets items count
*/
public int getItemsCount(); /**
* Gets a wheel item by index.
*
* @param index the item index
* @return the wheel item text or null
*/
public T getItem(int index); /**
* Gets maximum item length. It is used to determine the wheel width.
* If -1 is returned there will be used the default wheel width.
*
* @return the maximum item length or -1
*/
public int indexOf(T o);
}

  这是一个接口,它三个方法分别能够获取数据条数,当前索引下所对应条目,以及当前条目所对应索引。这都是对数据频繁操作。

  趁热打铁,我们更进一步,看看负责展示view又是长成什么样子。

public static DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm");
private View view;
private WheelView wv_year;
private WheelView wv_month;
private WheelView wv_day;
private WheelView wv_hours;
private WheelView wv_mins; private Type type;
private static int START_YEAR = 1990, END_YEAR = 2100; public View getView() {
return view;
} public void setView(View view) {
this.view = view;
} public static int getSTART_YEAR() {
return START_YEAR;
} public static void setSTART_YEAR(int sTART_YEAR) {
START_YEAR = sTART_YEAR;
} public static int getEND_YEAR() {
return END_YEAR;
} public static void setEND_YEAR(int eND_YEAR) {
END_YEAR = eND_YEAR;
} public WheelTime(View view) {
super();
this.view = view;
type = Type.ALL;
setView(view);
}
public WheelTime(View view,Type type) {
super();
this.view = view;
this.type = type;
setView(view);
}
public void setPicker(int year ,int month,int day){
this.setPicker(year, month, day, 0, 0);
} /**
* @Description: TODO 弹出日期时间选择器
*/
public void setPicker(int year ,int month ,int day,int h,int m) {
// 添加大小月月份并将其转换为list,方便之后的判断
String[] months_big = { "1", "3", "5", "7", "8", "10", "12" };
String[] months_little = { "4", "6", "9", "11" }; final List<String> list_big = Arrays.asList(months_big);
final List<String> list_little = Arrays.asList(months_little); Context context = view.getContext();
// 年
wv_year = (WheelView) view.findViewById(R.id.year);
wv_year.setAdapter(new NumericWheelAdapter(START_YEAR, END_YEAR));// 设置"年"的显示数据
wv_year.setLabel(context.getString(R.string.pickerview_year));// 添加文字
wv_year.setCurrentItem(year - START_YEAR);// 初始化时显示的数据 // 月
wv_month = (WheelView) view.findViewById(R.id.month);
wv_month.setAdapter(new NumericWheelAdapter(1, 12));
wv_month.setLabel(context.getString(R.string.pickerview_month));
wv_month.setCurrentItem(month); // 日
wv_day = (WheelView) view.findViewById(R.id.day);
// 判断大小月及是否闰年,用来确定"日"的数据
if (list_big.contains(String.valueOf(month + 1))) {
wv_day.setAdapter(new NumericWheelAdapter(1, 31));
} else if (list_little.contains(String.valueOf(month + 1))) {
wv_day.setAdapter(new NumericWheelAdapter(1, 30));
} else {
// 闰年
if ((year % 4 == 0 && year % 100 != 0) || year % 400 == 0)
wv_day.setAdapter(new NumericWheelAdapter(1, 29));
else
wv_day.setAdapter(new NumericWheelAdapter(1, 28));
}
wv_day.setLabel(context.getString(R.string.pickerview_day));
wv_day.setCurrentItem(day - 1); wv_hours = (WheelView)view.findViewById(R.id.hour);
wv_hours.setAdapter(new NumericWheelAdapter(0, 23));
wv_hours.setLabel(context.getString(R.string.pickerview_hours));// 添加文字
wv_hours.setCurrentItem(h); wv_mins = (WheelView)view.findViewById(R.id.min);
wv_mins.setAdapter(new NumericWheelAdapter(0, 59));
wv_mins.setLabel(context.getString(R.string.pickerview_minutes));// 添加文字
wv_mins.setCurrentItem(m); // 添加"年"监听
OnItemSelectedListener wheelListener_year = new OnItemSelectedListener() {
@Override
public void onItemSelected(int index) {
int year_num = index + START_YEAR;
// 判断大小月及是否闰年,用来确定"日"的数据
int maxItem = 30;
if (list_big
.contains(String.valueOf(wv_month.getCurrentItem() + 1))) {
wv_day.setAdapter(new NumericWheelAdapter(1, 31));
maxItem = 31;
} else if (list_little.contains(String.valueOf(wv_month
.getCurrentItem() + 1))) {
wv_day.setAdapter(new NumericWheelAdapter(1, 30));
maxItem = 30;
} else {
if ((year_num % 4 == 0 && year_num % 100 != 0)
|| year_num % 400 == 0){
wv_day.setAdapter(new NumericWheelAdapter(1, 29));
maxItem = 29;
}
else{
wv_day.setAdapter(new NumericWheelAdapter(1, 28));
maxItem = 28;
}
}
if (wv_day.getCurrentItem() > maxItem - 1){
wv_day.setCurrentItem(maxItem - 1);
}
}
};
// 添加"月"监听
OnItemSelectedListener wheelListener_month = new OnItemSelectedListener() {
@Override
public void onItemSelected(int index) {
int month_num = index + 1;
int maxItem = 30;
// 判断大小月及是否闰年,用来确定"日"的数据
if (list_big.contains(String.valueOf(month_num))) {
wv_day.setAdapter(new NumericWheelAdapter(1, 31));
maxItem = 31;
} else if (list_little.contains(String.valueOf(month_num))) {
wv_day.setAdapter(new NumericWheelAdapter(1, 30));
maxItem = 30;
} else {
if (((wv_year.getCurrentItem() + START_YEAR) % 4 == 0 && (wv_year
.getCurrentItem() + START_YEAR) % 100 != 0)
|| (wv_year.getCurrentItem() + START_YEAR) % 400 == 0){
wv_day.setAdapter(new NumericWheelAdapter(1, 29));
maxItem = 29;
}
else{
wv_day.setAdapter(new NumericWheelAdapter(1, 28));
maxItem = 28;
}
}
if (wv_day.getCurrentItem() > maxItem - 1){
wv_day.setCurrentItem(maxItem - 1);
} }
};
wv_year.setOnItemSelectedListener(wheelListener_year);
wv_month.setOnItemSelectedListener(wheelListener_month); // 根据屏幕密度来指定选择器字体的大小(不同屏幕可能不同)
int textSize = 6;
switch(type){
case ALL:
textSize = textSize * 3;
break;
case YEAR_MONTH_DAY:
textSize = textSize * 4;
wv_hours.setVisibility(View.GONE);
wv_mins.setVisibility(View.GONE);
break;
case HOURS_MINS:
textSize = textSize * 4;
wv_year.setVisibility(View.GONE);
wv_month.setVisibility(View.GONE);
wv_day.setVisibility(View.GONE);
break;
case MONTH_DAY_HOUR_MIN:
textSize = textSize * 3;
wv_year.setVisibility(View.GONE);
break;
case YEAR_MONTH:
textSize = textSize * 4;
wv_day.setVisibility(View.GONE);
wv_hours.setVisibility(View.GONE);
wv_mins.setVisibility(View.GONE);
}
wv_day.setTextSize(textSize);
wv_month.setTextSize(textSize);
wv_year.setTextSize(textSize);
wv_hours.setTextSize(textSize);
wv_mins.setTextSize(textSize); } /**
* 设置是否循环滚动
* @param cyclic
*/
public void setCyclic(boolean cyclic){
wv_year.setCyclic(cyclic);
wv_month.setCyclic(cyclic);
wv_day.setCyclic(cyclic);
wv_hours.setCyclic(cyclic);
wv_mins.setCyclic(cyclic);
}
public String getTime() {
StringBuffer sb = new StringBuffer();
sb.append((wv_year.getCurrentItem() + START_YEAR)).append("-")
.append((wv_month.getCurrentItem() + 1)).append("-")
.append((wv_day.getCurrentItem() + 1)).append(" ")
.append(wv_hours.getCurrentItem()).append(":")
.append(wv_mins.getCurrentItem());
return sb.toString();
}

  这个控件主要负责数据展示,因此他需要加载一个view视图,加载年月日视图的时候,我们要对相应的大小月,平闰年进行逻辑的判断,另外,为了其他地方能够很方便的调用,我们还需要将选择时间数据进行封装,嗯,这个控件就负责数据的展示,就这么简单。

  既然是自定义控件,前面都应该是一些铺垫,下面,我们需要对重头戏——wheelview进行一下庖丁解牛般的剖析。

  前面说了wheelview控件是个controller,那么他一定有什么独特逻辑在里面,其实无论是自己自定义控件也好,还是瞻仰像sai大神也罢,无非对控件的onDraw方法,onMeasure方法,OnTouch事件搞定,一个控件也就弄明白了。我们这里也把握这个节奏来分析wheelView的代码。

  从那里开始了,万事开头难,还是从最简单开始吧!

 @Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
this.widthMeasureSpec = widthMeasureSpec;
remeasure();
setMeasuredDimension(measuredWidth, measuredHeight);
} private void remeasure() {
if (adapter == null) {
return;
} measureTextWidthHeight(); //最大Text的高度乘间距倍数得到 可见文字实际的总高度,半圆的周长
halfCircumference = (int) (maxTextHeight * lineSpacingMultiplier * (itemsVisible - 1)) ;
//整个圆的周长除以PI得到直径,这个直径用作控件的总高度
measuredHeight = (int) ((halfCircumference * 2) / Math.PI);
//求出半径
radius = (int) (halfCircumference / Math.PI);
//控件宽度,这里支持weight
measuredWidth = MeasureSpec.getSize(widthMeasureSpec);
//计算两条横线和控件中间点的Y位置
firstLineY = (int) ((measuredHeight - lineSpacingMultiplier * maxTextHeight) / 2.0F);
secondLineY = (int) ((measuredHeight + lineSpacingMultiplier * maxTextHeight) / 2.0F);
centerY = (int) ((measuredHeight + maxTextHeight) / 2.0F - CENTERCONTENTOFFSET);
//初始化显示的item的position,根据是否loop
if (initPosition == -1) {
if (isLoop) {
initPosition = (adapter.getItemsCount() + 1) / 2;
} else {
initPosition = 0;
}
} preCurrentIndex = initPosition;
}

  这个方法主要作用就是重新测量子控件位置,由于子控件是以半圆形式进行排列的,我们需要计算出半圆的周长,以及半圆半径,以及中间两条横线与中间点y位置,以及控件初始化位置,这些变量将在后面ondraw方法绘制时候将有很大作用。

  既然能够滚动,就需要监听触摸事件,那么ontouch事件代码是怎么样的了:

    @Override
public boolean onTouchEvent(MotionEvent event) {
boolean eventConsumed = gestureDetector.onTouchEvent(event);
float itemHeight = lineSpacingMultiplier * maxTextHeight; switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
startTime = System.currentTimeMillis();
cancelFuture();
previousY = event.getRawY();
break; case MotionEvent.ACTION_MOVE:
float dy = previousY - event.getRawY();
previousY = event.getRawY(); totalScrollY = (int) (totalScrollY + dy); // 边界处理。
if (!isLoop) {
float top = -initPosition * itemHeight;
float bottom = (adapter.getItemsCount() - 1 - initPosition) * itemHeight; if (totalScrollY < top) {
totalScrollY = (int) top;
} else if (totalScrollY > bottom) {
totalScrollY = (int) bottom;
}
}
break; case MotionEvent.ACTION_UP:
default:
if (!eventConsumed) {
float y = event.getY();
double l = Math.acos((radius - y) / radius) * radius;
int circlePosition = (int) ((l + itemHeight / 2) / itemHeight); float extraOffset = (totalScrollY % itemHeight + itemHeight) % itemHeight;
mOffset = (int) ((circlePosition - itemsVisible / 2) * itemHeight - extraOffset); if ((System.currentTimeMillis() - startTime) > 120) {
// 处理拖拽事件
smoothScroll(ACTION.DAGGLE);
} else {
// 处理条目点击事件
smoothScroll(ACTION.CLICK);
}
}
break;
}
invalidate(); return true;
}

  在action_down中,我们需要记录用户按下坐标位置,再action_move中处理用户移动的边界值,这样做有什么好处了,就是移动条目最终都处于控件正中的位置,而在action_up,我们要确认用户到底使移动多大距离,这样子,调用重绘的方法,滚轮就会滚动相应距离,实现最终滚动效果。

  如果说wheelView是本控件的核心,那么ondraw方法就是wheelView代码核心,最高潮终于来了。

 protected void onDraw(Canvas canvas) {
if (adapter == null) {
return;
} //可见的item数组
Object visibles[] = new Object[itemsVisible];
//更加滚动的Y值高度除去每行Item的高度,得到滚动了多少个item,即change数
change = (int) (totalScrollY / (lineSpacingMultiplier * maxTextHeight));
try {
//滚动中实际的预选中的item(即经过了中间位置的item) = 滑动前的位置 + 滑动相对位置
preCurrentIndex = initPosition + change % adapter.getItemsCount();
}catch (ArithmeticException e){
System.out.println("出错了!adapter.getItemsCount() == 0,联动数据不匹配");
}
if (!isLoop) {//不循环的情况
if (preCurrentIndex < 0) {
preCurrentIndex = 0;
}
if (preCurrentIndex > adapter.getItemsCount() - 1) {
preCurrentIndex = adapter.getItemsCount() - 1;
}
} else {//循环
if (preCurrentIndex < 0) {//举个例子:如果总数是5,preCurrentIndex = -1,那么preCurrentIndex按循环来说,其实是0的上面,也就是4的位置
preCurrentIndex = adapter.getItemsCount() + preCurrentIndex;
}
if (preCurrentIndex > adapter.getItemsCount() - 1) {//同理上面,自己脑补一下
preCurrentIndex = preCurrentIndex - adapter.getItemsCount();
}
} //跟滚动流畅度有关,总滑动距离与每个item高度取余,即并不是一格格的滚动,每个item不一定滚到对应Rect里的,这个item对应格子的偏移值
int itemHeightOffset = (int) (totalScrollY % (lineSpacingMultiplier * maxTextHeight));
// 设置数组中每个元素的值
int counter = 0;
while (counter < itemsVisible) {
int index = preCurrentIndex - (itemsVisible / 2 - counter);//索引值,即当前在控件中间的item看作数据源的中间,计算出相对源数据源的index值
//判断是否循环,如果是循环数据源也使用相对循环的position获取对应的item值,如果不是循环则超出数据源范围使用""空白字符串填充,在界面上形成空白无数据的item项
if (isLoop) {
if (index < 0) {
index = index + adapter.getItemsCount();
if(index < 0){
index = 0;
}
}
if (index > adapter.getItemsCount() - 1) {
index = index - adapter.getItemsCount();
if (index > adapter.getItemsCount() - 1){
index = adapter.getItemsCount() - 1;
}
}
visibles[counter] = adapter.getItem(index);
} else if (index < 0) {
visibles[counter] = "";
} else if (index > adapter.getItemsCount() - 1) {
visibles[counter] = "";
} else {
visibles[counter] = adapter.getItem(index);
}
counter++; } //中间两条横线
canvas.drawLine(0.0F, firstLineY, measuredWidth, firstLineY, paintIndicator);
canvas.drawLine(0.0F, secondLineY, measuredWidth, secondLineY, paintIndicator);
//单位的Label
if(label != null) {
int drawRightContentStart = measuredWidth - getTextWidth(paintCenterText,label);
//靠右并留出空隙
canvas.drawText(label, drawRightContentStart - CENTERCONTENTOFFSET, centerY, paintCenterText);
}
counter = 0;
while (counter < itemsVisible) {
canvas.save();
// L(弧长)=α(弧度)* r(半径) (弧度制)
// 求弧度--> (L * π ) / (π * r) (弧长X派/半圆周长)
float itemHeight = maxTextHeight * lineSpacingMultiplier;
double radian = ((itemHeight * counter - itemHeightOffset) * Math.PI) / halfCircumference;
// 弧度转换成角度(把半圆以Y轴为轴心向右转90度,使其处于第一象限及第四象限
float angle = (float) (90D - (radian / Math.PI) * 180D);
if (angle >= 90F || angle <= -90F) {
canvas.restore();
} else {
String contentText = getContentText(visibles[counter]); //计算开始绘制的位置
measuredCenterContentStart(contentText);
measuredOutContentStart(contentText);
int translateY = (int) (radius - Math.cos(radian) * radius - (Math.sin(radian) * maxTextHeight) / 2D);
//根据Math.sin(radian)来更改canvas坐标系原点,然后缩放画布,使得文字高度进行缩放,形成弧形3d视觉差
canvas.translate(0.0F, translateY);
canvas.scale(1.0F, (float) Math.sin(radian));
if (translateY <= firstLineY && maxTextHeight + translateY >= firstLineY) {
// 条目经过第一条线
canvas.save();
canvas.clipRect(0, 0, measuredWidth, firstLineY - translateY);
canvas.scale(1.0F, (float) Math.sin(radian) * SCALECONTENT);
canvas.drawText(contentText, drawOutContentStart, maxTextHeight, paintOuterText);
canvas.restore();
canvas.save();
canvas.clipRect(0, firstLineY - translateY, measuredWidth, (int) (itemHeight));
canvas.scale(1.0F, (float) Math.sin(radian) * 1F);
canvas.drawText(contentText, drawCenterContentStart, maxTextHeight - CENTERCONTENTOFFSET, paintCenterText);
canvas.restore();
} else if (translateY <= secondLineY && maxTextHeight + translateY >= secondLineY) {
// 条目经过第二条线
canvas.save();
canvas.clipRect(0, 0, measuredWidth, secondLineY - translateY);
canvas.scale(1.0F, (float) Math.sin(radian) * 1.0F);
canvas.drawText(contentText, drawCenterContentStart, maxTextHeight - CENTERCONTENTOFFSET, paintCenterText);
canvas.restore();
canvas.save();
canvas.clipRect(0, secondLineY - translateY, measuredWidth, (int) (itemHeight));
canvas.scale(1.0F, (float) Math.sin(radian) * SCALECONTENT);
canvas.drawText(contentText, drawOutContentStart, maxTextHeight, paintOuterText);
canvas.restore();
} else if (translateY >= firstLineY && maxTextHeight + translateY <= secondLineY) {
// 中间条目
canvas.clipRect(0, 0, measuredWidth, (int) (itemHeight));
canvas.drawText(contentText, drawCenterContentStart, maxTextHeight - CENTERCONTENTOFFSET, paintCenterText);
int preSelectedItem = adapter.indexOf(visibles[counter]);
if(preSelectedItem != -1){
selectedItem = preSelectedItem;
}
} else {
// 其他条目
canvas.save();
canvas.clipRect(0, 0, measuredWidth, (int) (itemHeight));
canvas.scale(1.0F, (float) Math.sin(radian) * SCALECONTENT);
canvas.drawText(contentText, drawOutContentStart, maxTextHeight, paintOuterText);
canvas.restore();
}
canvas.restore();
}
counter++;
}
}

  前文提到过,根据用户滚动的距离,计算出最终滚动到条目,然后把这条目周围一定数目的数据绘制出来,最难理解,就是到底如何绘制的了,我们可以这样理解根据位置来计算应该把画布哪部分进行缩放,上下就是压小高度,中间放大高度,然后下面判断这个文字位置到哪里了,超过第一条线就是说明进入中间位置,放大文字,超过第二条线说明出了中间位置,缩回去。也许,你还会问,他y轴是如何移动的,它移动距离其实就是你可以理解为一条线曲成弧形,然后根据在这条线上的位置计算出这个弧形上面的位置再折射出这个弧形对应一条竖的射影位置。示意图如下:

  这就是我对这个控件一点心得。

 

老猪带你玩转自定义控件三——sai大神带我实现ios 8 时间滚轮控件的更多相关文章

  1. ASP.NET自定义控件组件开发 第一章 第一章:从一个简单的控件谈起

    第一章:从一个简单的控件谈起 系列文章链接: ASP.NET自定义控件组件开发 第一章 待续 ASP.NET自定义控件组件开发 第一章 第二篇 接着待续 ASP.NET自定义控件组件开发 第一章 第三 ...

  2. TOM带你玩充电 篇三:15款5号电池横评及选购建议——南孚金霸王小米宜家耐时品胜一个都逃不了

    双鹿电池的几个版本 理论上来说性价比:绿骑士>金骑士>黑骑士>蓝骑士 绿骑士和金骑士都很不错.哪个便宜买哪个. 小米性价比虽然最高,但是超市买不到. 蓝骑士是普通碳性电池,黑骑士是高 ...

  3. 深入浅出带你玩转sqlilabs(三)--GET,POST,COOKIE,万能密码注入

    常见提交方式下的注入漏洞 WEB应用在数据传递接受中,针对SQL注入安全漏洞,由于数据大小,格式等原因,脚本在接受传递时会有多种传递方式,传递方式的不同将影响到安全测试的不同 第一点:数据常见提交方式 ...

  4. Java大神带你领略queue的风采

    作为数据结构中比较常见的类型,你足够了解队列(queue)吗?从今天开始,我将为你讲解关于队列(queue)的一切,包括概念.类型和具体使用方法,如果你对此足够感兴趣,赶快来加入我们,我将同你一起探索 ...

  5. 大神带你一天了解zabbix(一)

    第15章 Zabbix的搭建 15.1 为什么使用监控服务 对系统实现不间断的监控,实现报警通知(电话,微信,邮件,发短信,手环) 实时反馈系统当前的状态信息 保证服务的可靠安全性 保证业务的稳定运行 ...

  6. 【附案例】UI交互设计不会做?设计大神带你开启动效灵感之路

    随着网络技术的创新发展,如今UI交互设计应用越来越广泛,显然已经成为设计的主流及流行的必然趋势.UI界面交互设计中的动效包括移动,滑块,悬停效果,GIF动画等.UI界面交互设计为何越来越受到青睐?它有 ...

  7. android动手写控件系列——老猪叫你写相机

    前记:Android这个开源而自由的系统,为我们带来开发便利,同时也埋下太多的深坑.例如调用系统自带的相机就会出现照片丢失,或者其他各种各样的问题.因此,看来自定义一个相机十分的必要. 要自定义相机我 ...

  8. Android自定义View(三、深入解析控件测量onMeasure)

    转载请标明出处: http://blog.csdn.net/xmxkf/article/details/51490283 本文出自:[openXu的博客] 目录: onMeasure什么时候会被调用 ...

  9. WPF教程十二:了解自定义控件的基础和自定义无外观控件

    这一篇本来想先写风格主题,主题切换.自定义配套的样式.但是最近加班.搬家.新租的房子打扫卫生,我家宝宝6月中旬要出生协调各种的事情,导致了最近精神状态不是很好,又没有看到我比较喜欢的主题风格去模仿的, ...

随机推荐

  1. ScrollView中嵌套GridView,Listview的办法

    按照android的标准,ScrollView中是不能嵌套具有滑动特性的View的,但是有时如果设计真的有这样做的需要,或者为了更方便简单的实现外观(比如在外在的大布局需要有滑动的特性,并且内部有类似 ...

  2. B+/-Tree原理

    B-Tree介绍 B-Tree是一种多路搜索树(并不是二叉的):       1.定义任意非叶子结点最多只有M个儿子:且M>2:       2.根结点的儿子数为[2, M]:       3. ...

  3. HDU 4816 Bathysphere (2013长春现场赛D题)

    题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=4816 2013长春区域赛的D题. 很简单的几何题,就是给了一条折线. 然后一个矩形窗去截取一部分,求最 ...

  4. Synchronize Ultimate

    支持多种服务器和主流云网盘进行同步 http://www.icecoldapps.com/ Unlock Code : xda201506 Unlock Code : icecoldapps20150 ...

  5. react-router 从 v2/v3 to v4 迁移(翻译)

    react-router v4 是完全重写的,所以没有简单的迁移方式,这份指南将为您提供一些步骤,以帮助您了解如何升级应用程序. 注意: 这份迁移指南适用于react-router v2和v3,但为简 ...

  6. firedac连接linux的oracle

    firedac连接linux的oracle 首先要下载并安装 oci for linux.OCI的版本要和ORACLE数据库的版本一致. centos7环境下安装OCI:https://blog.cs ...

  7. 【linux】linux 下 shell命令 执行结果赋值给变量【两种方式】

    方法1:[通用方法] 使用Tab键上面的反引号 例子如下: find命令 模糊查询在/apps/swapping目录下 查找 文件名中包含swapping并且以.jar结尾的文件 使用反引号 引住命令 ...

  8. Cannot subclass final class class com.sun.proxy.$Proxy

    背景 这个错误是我在使用AOP动态切换数据库,实现数据库的读写分离的时候出现的问题,使用到的系统环境是: <spring.version>3.2.6.RELEASE</spring. ...

  9. Java中判断String不为空的问题性能比较

    function 1: 最多人使用的一个方法, 直观, 方便, 但效率很低. function 2: 比较字符串长度, 效率高, 是我知道的最好一个方法. function 3: Java SE 6. ...

  10. 云服务器 ECS Linux 系统安装图形化桌面 (centos7 ubuntu14)

    基于性能及通用性等因素,默认情况下,阿里云官方公共 Linux 系统镜像不会安装图形化桌面组件.本文简述了常用操作系统图形化桌面安装说明,您可以根据需求进行配置系统. 注意:图形化桌面可能会 显著降低 ...