@Override public void onSubscribe(Disposable d) {
}
@SuppressLint("SetTextI18n") @Override public void onNext(WeatherBean weatherBean) { //在主线程中处理得到的数据 }
@Override public void onError(Throwable e) {
}
@Override public void onComplete() {
} }); }
###自定义View 布局中间展示未来15天天气,数据有日期、最高温度、最低温度、类型、类型图标,其中温度连成两条曲线,整体支持滑动。
我是这样设计的,温度曲线初始为两条直线,为这15天的平均值,然后开始变化,变到对应的值,从而形成曲线效果。
新建MyCurveView.java,继承自View。添加WeatherData内部类,添加对应的属性及get、set方法。
static class WeatherData { private float lowTemp; private float highTemp; private int date; private String type; private Bitmap typeBitmap;
WeatherData(float lowTemp, float highTemp, int date, String type, Bitmap typeBitmap) { this.lowTemp = lowTemp; this.highTemp = highTemp; this.date = date; this.type = type; this.typeBitmap = typeBitmap; }
... }
添加setProgress()方法,在网络请求完毕后,调用该方法更新数据和UI。首先调用arrayList保存网络数据,然后在动画中不断更新视图。
public void setProgress(int averageHigh, int averageLow, final int low, int top, ArrayList innerData) { arrayList(innerData, top, low, averageHigh, averageLow); ValueAnimator animatorHigh = ValueAnimator.ofInt(0, top); animatorHigh.setDuration(1000); animatorHigh.setInterpolator(new AccelerateInterpolator()); animatorHigh.addUpdateListener(valueAnimator -> { mHighPercent = (int)valueAnimator.getAnimatedValue(); invalidate(); });
ValueAnimator animatorLow = ValueAnimator.ofInt(0, low); animatorLow.setDuration(1000); animatorLow.setInterpolator(new AccelerateInterpolator()); animatorLow.addUpdateListener(valueAnimator -> { mLowPercent = (int)valueAnimator.getAnimatedValue(); });
AnimatorSet set = new AnimatorSet(); //两个动画同时进行 set.playTogether(animatorHigh, animatorLow); //监听动画 set.addListener(new Animator.AnimatorListener() { @Override public void onAnimationStart(Animator animation) { //在做动画的时间内,通过该标志位禁止触摸动作 isAnimation = true; }
@Override public void onAnimationEnd(Animator animation) { isAnimation = false; }
@Override public void onAnimationCancel(Animator animation) {
}
@Override public void onAnimationRepeat(Animator animation) {
} }); set.start(); }
arrayList()方法,除了保存数据外,将温度做个转换,因为初始是从平均值开始变的,mHighPercent在1s的时间内从0变为15日最高温度值,mHighPercent * (innerData.get(i).getHighTemp() – averageHigh) / (max – 0)可以做到在1s的时间内,将当日最高温度从平均值变为实际值,当日最低温度同理。
@SuppressWarnings("PointlessArithmeticExpression") private void arrayList(ArrayList innerData, int max, int min, int averageHigh, int averageLow) { high = averageHigh; low = averageLow;
dataArray.clear(); //保存网络数据 dataArray.addAll(innerData);
for (int i = 0; i < innerData.size(); i++) { //在1s的变化时间内,将值从平均值变为实际值 dataArray.get(i).setHighTemp((innerData.get(i).getHighTemp() - averageHigh) / (max - 0)); //在1s的变化时间内,将值从平均值变为实际值 dataArray.get(i).setLowTemp((averageLow - innerData.get(i).getLowTemp()) / (min - 0)); } }
重写onMeasure()。
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int widthSize = MeasureSpec.getSize(widthMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); measureWidth = widthSize; measureHeight = heightSize; //一个页面展示6天的温度信息,每一天为宽度为mTempWidth mTempWidth = measureWidth / 6; setMeasuredDimension(widthSize, heightSize); }
重写onDraw()。
@SuppressLint("DrawAllocation") @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); //为请求到网络数据时,页面显示文字 if (dataArray.size() <= 0) { drawNoDataText(canvas); } else { float startX = mStartX + (mTempWidth / 2); for (int i = 0; i < dataArray.size(); i++) { //绘制天气图标 RectF rectF = new RectF(startX - 30, 300, startX + 30, 360); canvas.drawBitmap(dataArray.get(i).getTypeBitmap(), null, rectF, mCurvePaint);
//绘制最高温度 float highTextWidth = mTempTextPaint.measureText((int)(high + mHighPercent * dataArray.get(i).getHighTemp()) + ""); float highTextStartX = startX - highTextWidth / 2; drawTempText(canvas, (int)(high + getHighTempByPercent(i)) + "", highTextStartX, (140 - curve_ratio * getHighTempByPercent(i))); //curve_ratio为可变值,用于调整显示效果。该值越大温度差效果越明显。 canvas.drawCircle(startX, (160 - curve_ratio * getHighTempByPercent(i)), 5, circlePaint); //第一段曲线 if (i == 0) { highPath.moveTo(startX, (160 - curve_ratio * getHighTempByPercent(i))); highControlPt1X = startX + mTempWidth / 4; highControlPt1Y = 160 - curve_ratio * getHighTempByPercent(i); highControlPt2X = startX + (mTempWidth / 4) * 3; highControlPt2Y = ((160 - curve_ratio * getHighTempByPercent(i + 1))) - (((160 - curve_ratio * getHighTempByPercent(i + 2))) - ((160 - curve_ratio * getHighTempByPercent(i)))) / 4; //3阶贝塞尔曲线 highPath.cubicTo( highControlPt1X, highControlPt1Y, highControlPt2X, highControlPt2Y, startX + mTempWidth, (160 - curve_ratio * getHighTempByPercent(i + 1))); canvas.drawPath(highPath, mCurvePaint); //每次绘制后将画笔恢复 highPath.reset(); } //中间曲线 if (i != 0 && i < dataArray.size() - 2) { highPath.moveTo(startX, (160 - curve_ratio * getHighTempByPercent(i))); highControlPt1X = startX + mTempWidth / 4; highControlPt1Y = ((160 - curve_ratio * getHighTempByPercent(i))) + (((160 - curve_ratio * getHighTempByPercent(i + 1))) - ((160 - curve_ratio * getHighTempByPercent(i - 1)))) / 4; highControlPt2X = startX + (mTempWidth / 4) * 3; highControlPt2Y = ((160 - curve_ratio * getHighTempByPercent(i + 1))) - (((160 - curve_ratio * getHighTempByPercent(i + 2))) - ((160 - curve_ratio * getHighTempByPercent(i)))) / 4; highPath.cubicTo( highControlPt1X, highControlPt1Y, highControlPt2X, highControlPt2Y, startX + mTempWidth, (160 - curve_ratio * getHighTempByPercent(i + 1))); canvas.drawPath(highPath, mCurvePaint); highPath.reset(); } //最后一段曲线 if (i == dataArray.size() - 2) { highPath.moveTo(startX, (160 - curve_ratio * getHighTempByPercent(i))); highControlPt1X = startX + mTempWidth / 4; highControlPt1Y = ((160 - curve_ratio * getHighTempByPercent(i))) + (((160 - curve_ratio * getHighTempByPercent(i + 1))) - ((160 - curve_ratio * getHighTempByPercent(i - 1)))) / 4; highControlPt2X = startX + (mTempWidth / 4) * 3; highControlPt2Y = 160 - curve_ratio * getHighTempByPercent(i + 1); highPath.cubicTo( highControlPt1X, highControlPt1Y, highControlPt2X, highControlPt2Y, startX + mTempWidth, (160 - curve_ratio * getHighTempByPercent(i + 1))); canvas.drawPath(highPath, mCurvePaint); highPath.reset(); }
//绘制最低温度,与绘制最高温度类似 ...
//绘制日期 float dayTextWidth = mTextPaint.measureText(dataArray.get(i).getDate() + "日"); float dayStartX = startX - dayTextWidth / 2; float dayTextStartY = 40 + getFontAscentHeight(mTextPaint); drawDayText(canvas, dataArray.get(i).getDate() + "日", dayStartX, dayTextStartY);
//绘制天气 float typeTextWidth = mTextPaint.measureText(dataArray.get(i).getType()); float typeTextStartX = startX - typeTextWidth / 2; float typeTextStartY = measureHeight - 40 - getFontDescentHeight(mTextPaint); canvas.drawText(dataArray.get(i).getType(), typeTextStartX, typeTextStartY, mTextPaint); //每绘制完一天,往后移动mTempWidth距离,绘制下一天 startX = startX + mTempWidth; } } }
其中的一些参数是可以根据需要更改的。
重写dispatchTouchEvent()。当滑动到最左边也就是第一天的时候,应该禁止继续向右继续滑动。滑动到最右边同理。
@Override public boolean dispatchTouchEvent(MotionEvent ev) { int dispatchCurrX = (int) ev.getX(); int dispatchCurrY = (int) ev.getY(); switch (ev.getAction()) { ...
case MotionEvent.ACTION_MOVE: float deltaX = dispatchCurrX - dispatchTouchX; float deltaY = dispatchCurrY - dispatchTouchY; //竖直滑动的父容器拦截事件 if (Math.abs(deltaY) - Math.abs(deltaX) > 0) { getParent().requestDisallowInterceptTouchEvent(false); } //向右滑动,滑动到左边边界,父容器进行拦截 if ((dispatchCurrX - dispatchTouchX) > 0 && mStartX == 0) { getParent().requestDisallowInterceptTouchEvent(false); } else if ((dispatchCurrX - dispatchTouchX) < 0 && mStartX == -getMoveLength()) { //向左滑动,滑动到右边边界,父容器进行拦截 getParent().requestDisallowInterceptTouchEvent(false); } break;
... } dispatchTouchX = dispatchCurrX; dispatchTouchY = dispatchCurrY; return super.dispatchTouchEvent(ev); }
重写onTouchEvent()。
@SuppressLint("ClickableViewAccessibility") @Override public boolean onTouchEvent(MotionEvent event) { //如果正在执行动画,直接返回 if (isAnimation) { return true; } if (mVelocityTracker == null) { mVelocityTracker = VelocityTracker.obtain(); } mVelocityTracker.addMovement(event); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: lastX = event.getX(); //当点击的时候,判断如果是在fling的效果的时候,就停止快速滑动 if (isFling) { removeCallbacks(mScrollRunnable); isFling = false; } break;
case MotionEvent.ACTION_MOVE: float currX = event.getX(); mStartX += currX - lastX; //计算每次滑动后的mStartX //向右滑动 if ((currX - lastX) > 0) { if (mStartX > 0) { mStartX = 0; } } else {//向左滑动 if (-mStartX > getMoveLength()) { mStartX = -getMoveLength(); } } lastX = currX; break;
case MotionEvent.ACTION_UP: //1s内的滑动速度 mVelocityTracker.computeCurrentVelocity(1000); //计算快速滑动的速度,如果是大于某个值,并且数据的长度大于整个屏幕的长度,那么就允许有fling后逐渐停止的效果 if (Math.abs(mVelocityTracker.getXVelocity()) > 100 && !isFling && measureWidth < dataArray.size() * mTempWidth) { mScrollRunnable = new ScrollRunnable(mVelocityTracker.getXVelocity() / 5); this.post(mScrollRunnable); } break;
case MotionEvent.ACTION_CANCEL: break; } return true; }
private class ScrollRunnable implements Runnable {
private float speed;
ScrollRunnable(float speed) { this.speed = speed; }
@Override public void run() { if (Math.abs(speed) < 60) { isFling = false; return; } isFling = true; mStartX += speed / 15; //速度有一个渐慢的效果 speed = speed / 1.1f; //向右滑动 if ((speed) > 0) { if (mStartX > 0) { mStartX = 0; } } else { //向右滑动 if (-mStartX > getMoveLength()) { mStartX = -getMoveLength(); } } postDelayed(this, 5); invalidate(); } }
以上为部分主要代码,自定义View就算是完成了。
###城市搜索 使用单独一个Activity,使用了DataBinding来做搜索编辑框的绑定,RecyclerView用来展示返回的城市列表,选择其中的某一城市后,通过EventBus将城市信息通知MainActivity。新建CityActivity,添加CityViewHolder类,并在其中添加afterTextChanged(Editable s)方法,在onCreate中完成代码和视图的绑定
public class CityActivity extends AppCompatActivity {
...
@Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); //绑定代码和视图 ActivityCityBinding activityCityBinding = DataBindingUtil.setContentView(this, R.layout.activity_city); activityCityBinding.setCityViewHolder(new CityViewHolder());
... }
...
public class CityViewHolder {
public void afterTextChanged(Editable s) {
} } }
修改对应的activity_city.xml
...
这样,每次修改EditText都会走afterTextChanged()。
接下完成搜索城市的请求。新建CityBean.java,和上面一样,通过GsonFormat自动生成代码,Json数据可以在和风天气的接口文档的数据返回示例中看到。接下来新建CityService.java,添加getCall方法。查看和风天气的接口文档,location和key是必选的,通过@Query注解添加请求URL中的这两个参数。返回类型中的泛型为刚刚完成的CityBean类。
public interface CityService {
@GET("find") Observable getCall(@Query("location") String location, @Query("key") String key);
}
实现afterTextChanged(),和上面天气数据请求基本一致。
public void afterTextChanged(Editable s) { ... //如果输入为空,直接返回 if (s.toString().equals("")) { return; }
Retrofit retrofit = new Retrofit.Builder() .baseUrl("search.heweather.net/") .addConverterFactory(GsonConverterFactory.create())//使用Gson .addCallAdapterFactory(RxJava2CallAdapterFactory.create())//使用RxJava .build();
CityService cityService = retrofit.create(CityService.class); cityService.getCall(s.toString(), KEY) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Observer() { @Override public void onSubscribe(Disposable d) {
}
@Override public void onNext(CityBean cityBean) { //对获取到的数据进行处理 }
@Override public void onError(Throwable e) {
}
@Override public void onComplete() { ... } });
}
新建CitysAdapter.java,继承自RecyclerView.Adapter,作为RecyclerView的Adapter。
public class CitysAdapter extends RecyclerView.Adapter<CitysAdapter.ViewHolder>{
private ArrayList citys;
private Context mContext;
最后
都说三年是程序员的一个坎,能否晋升或者提高自己的核心竞争力,这几年就十分关键。
技术发展的这么快,从哪些方面开始学习,才能达到高级工程师水平,最后进阶到Android架构师/技术专家?我总结了这 5大块;
我搜集整理过这几年阿里,以及腾讯,字节跳动,华为,小米等公司的面试题,把面试的要求和技术点梳理成一份大而全的“ Android架构师”面试 Xmind(实际上比预期多花了不少精力),包含知识脉络 + 分支细节。
《Android架构视频+BAT面试专题PDF+学习笔记》
网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。希望**这份系统化的技术体系**对大家有一个方向参考。
2021年虽然路途坎坷,都在说Android要没落,但是,不要慌,做自己的计划,学自己的习,竞争无处不在,每个行业都是如此。相信自己,没有做不到的,只有想不到的。祝大家2021年万事大吉。