implementation ‘com.jakewharton:butterknife:9.0.0’
implementation ‘com.squareup.okhttp3:okhttp:3.10.0’
implementation ‘org.greenrobot:eventbus:3.0.0’
annotationProcessor ‘com.jakewharton:butterknife-compiler:9.0.0’
…
}
在project的build.gradle中添加
dependencies {
…
classpath ‘com.jakewharton:butterknife-gradle-plugin:9.0.0’
classpath ‘com.neenbedankt.gradle.plugins:android-apt:1.8’
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
#####天气数据网络请求
分为两块,一块是天气和图标的请求,使用RxJava的flatMap操作符将两者连在一起:首先请求天气数据,得到数据后使用flatMap操作符,取出数据中的天气类型进行第二次网络请求,最后在主线程中处理数据。第二块是搜索城市返回城市代码,一个简单的Retrofit + RxJava就可实现。
写Retrofit中的service接口。新建WeatherService.java,类型为interface。添加getCall方法,GET请求,因为接口为http://t.weather.sojson.com/api/weather/city/+city_code。每次请求改变city_code即可,通过@Path注解实现。类型Observable< WeatherBean >,其中Observable是因为用了RxJava,WeatherBean就是前面生成的Bean类。
public interface WeatherService {
//每次请求city_code可变
@GET(“{city_code}”)
Observable getCall(@Path(“city_code”) String code);
}
在MainActivity中使用Retrofit,添加requestWeather(String cityId)方法。
@SuppressLint(“CheckResult”)
private void requestWeather(String cityId) {
bitmaps.clear();
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(“http://t.weather.sojson.com/api/weather/city/”)
.addConverterFactory(GsonConverterFactory.create())//使用Gson
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())//使用RxJava
.build();
final WeatherService weatherService = retrofit.create(WeatherService.class);
//第一次网络请求,获取天气数据
weatherService.getCall(cityId)
.subscribeOn(Schedulers.io())
.flatMap((Function<WeatherBean, Observable>) weatherBean -> {
…
for (WeatherBean.DataBean.ForecastBean forecastBean : weatherBean.getData().getForecast()) {
…
//循环取出每一天的天气类型,添加url
String url = “https://cdn.heweather.com/cond_icon/” + preferences.getString(forecastBean.getType(), “未知”);
//使用Glide通过天气类型进行第二次网络请求,获取类型对应天气图标bitmap
FutureTarget target = Glide.with(getApplicationContext())
.asBitmap()
.load(url)
.submit();
final Bitmap bitmap = target.get();
dataArrayList.add(new MyCurveView.WeatherData(low, high, date, type, bitmap));
}
…
//将请求返回的天气数据继续传至主线程的Observer观察者
return Observable.fromArray(weatherBean);
})
.observeOn(AndroidSchedulers.mainThread())//切换至主线程
.subscribe(new Observer() {
@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
<?xml version="1.0" encoding="utf-8"?>自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)
![](https://i-blog.csdnimg.cn/blog_migrate/26f24562869aa5a3df1d7f79047d9978.jpeg)
更多Android高级工程师进阶学习资料
进阶学习视频
附上:我们之前因为秋招收集的二十套一二线互联网公司Android面试真题(含BAT、小米、华为、美团、滴滴)和我自己整理Android复习笔记(包含Android基础知识点、Android扩展知识点、Android源码解析、设计模式汇总、Gradle知识点、常见算法题汇总。)
里面包含不同方向的自学编程路线、面试题集合/面经、及系列技术文章等,资源持续更新中…
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!
24/03/13/H4lCoPEF.jpg" />
更多Android高级工程师进阶学习资料
进阶学习视频
[外链图片转存中…(img-rUuhhdii-1712593079061)]
附上:我们之前因为秋招收集的二十套一二线互联网公司Android面试真题(含BAT、小米、华为、美团、滴滴)和我自己整理Android复习笔记(包含Android基础知识点、Android扩展知识点、Android源码解析、设计模式汇总、Gradle知识点、常见算法题汇总。)
[外链图片转存中…(img-WcoSlT7X-1712593079061)]
里面包含不同方向的自学编程路线、面试题集合/面经、及系列技术文章等,资源持续更新中…
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!