写点什么

Android 简易天气 App

用户头像
Android架构
关注
发布于: 16 小时前

.subscribe(new Observer<WeatherBean>() {


@Overridepublic void onSubscribe(Disposable d) {


}


@SuppressLint("SetTextI18n")@Overridepublic void onNext(WeatherBean weatherBean) {//在主线程中处理得到的数据}


@Overridepublic void onError(Throwable e) {


}


@Overridepublic 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<WeatherData> 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() {@Overridepublic void onAnimationStart(Animator animation) {//在做动画的时间内,通过该标志位禁止触摸动作 isAnimation = true;}


@Overridepublic void onAnimationEnd(Animator animation) {isAnimation = false;}


@Overridepublic void onAnimationCancel(Animator animation) {


}


@Overridepublic 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<WeatherData> 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()。


@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {int widthSize = MeasureSpec.getSize(widthMeasureSpec);int heightSize = MeasureSpec.getSize(heightMeasureSpec);measureWidth = widthSize;measureHeight = heightSize;//一个页面展示 6 天的温度信息,每一天为宽度为 mTempWidthmTempWidth = measureWidth / 6;setMeasuredDimension(widthSize, heightSize);}


重写 onDraw()。


@SuppressLint("DrawAllocation")@Overrideprotected 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 * getHig


《Android学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》
浏览器打开:qq.cn.hn/FTe 免费领取
复制代码


hTempByPercent(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()。当滑动到最左边也就是第一天的时候,应该禁止继续向右继续滑动。滑动到最右边同理。


@Overridepublic 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")@Overridepublic 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;}


@Overridepublic 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 {


...


@Overrideprotected 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"?><layout xmlns:android="http://schemas.android.com/apk/res/android">


<data>


<variablename="cityPresenter"type="com.sk.simpleweather.CityActivity.CityViewHolder" />


</data>


<LinearLayoutandroid:layout_width="match_parent"android:layout_height="match_parent"android:layout_marginTop="20dp"android:layout_marginStart="30dp"android:layout_marginEnd="30dp"android:orientation="vertical">


<EditTextandroid:id="@+id/search_edittext"android:layout_width="match_parent"android:layout_height="wrap_content"android:padding="5dp"android:background="@drawable/et_bgd"android:focusable="true"android:textSize="16sp"android:maxLines="1"android:singleLine="true"android:hint="输入城市"android:textColorHint="#40000000"android:afterTextChanged="@{cityPresenter.afterTextChanged}"/>


...


</LinearLayout></layout>


这样,每次修改 EditText 都会走 afterTextChanged()。


接下完成搜索城市的请求。新建 CityBean.java,和上面一样,通过 GsonFormat 自动生成代码,Json 数据可以在和风天气的接口文档的数据返回示例中看到。接下来新建 CityService.java,添加 getCall 方法。查看和风天气的接口文档,location 和 key 是必选的,通过 @Query 注解添加请求 URL 中的这两个参数。返回类型中的泛型为刚刚完成的 CityBean 类。


public interface CityService {


@GET("find")Observable<CityBean> 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("https://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<CityBean>() {@Overridepublic void onSubscribe(Disposable d) {


}


@Overridepublic void onNext(CityBean cityBean) {//对获取到的数据进行处理}


@Overridepublic void onError(Throwable e) {


}


@Overridepublic void onComplete() {...}});


}


新建 CitysAdapter.java,继承自 RecyclerView.Adapter,作为 RecyclerView 的 Adapter。


public class CitysAdapter extends RecyclerView.Adapter<CitysAdapter.ViewHolder>{


private ArrayList<String> citys;

用户头像

Android架构

关注

还未添加个人签名 2021.10.31 加入

还未添加个人简介

评论

发布
暂无评论
Android简易天气App