Android簡易天氣App

2021-02-14 Android編程精選
本打算是寫一個貝塞爾曲線的demo,想了一下哪種場景可以直觀的表現出貝塞爾曲線,想到天氣預報中的那些24小時和未來幾日天氣變化正好適用。接著開始構思,開始是打算把數據寫死的,然後想了想既然是模擬天氣預報,為了真實一點,乾脆就從網絡獲取吧,就找了個天氣接口,一看接口還有好多其他的數據,乾脆都用了吧。然後又想了一想,既然已經做成天氣預報了,切換城市是必須要有的吧。最後就變成了一個簡易的天氣App。先上效果圖,上面和下面兩部分是顯示的一些基本數據,中間那個可滑動的未來15日天氣就是一開始打算寫的貝塞爾曲線部分。選擇左上角的城市,會跳轉到搜索界面,可以搜索想要查看城市的天氣狀況。使用到的知識準備工作一共使用了3個接口,一個用來請求天氣數據,一個用來請求天氣類型的圖標,一個用來搜索城市。請求天氣的接口為http://t.weather.sojson.com/api/weather/city/city_code,接口及返回Json數據示例https://www.sojson.com/blog/305.html有說明,其中的city_code通過搜索城市的接口得到,使用的是和風天氣的接口。返回數據中的天氣類型用來請求天氣類型圖標,也是和風天氣的接口。和風天氣的官網上有說明。請求天氣接口返回的數據是Json格式的,這就需要做Json的解析,我這裡用的是Gson。使用Gson是需要與數據相對應的Bean類的,Android Studio中正好有一個插件叫做GsonFormat,可以自動生成對應的Bean類。選擇File Settings/Plugins/MarketPlace,搜索GsonFormat,安裝就可以了,我這個是已經安裝過了。在app的build.gradle中添加要使用到的依賴。直接都添加好了,已經將後續所有用到的都添加了好了。順便添加compileOptions和dataBinding兩段代碼。

android {
    ...
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }

    dataBinding {
        enabled = true
    }
}

dependencies {
    ...
    implementation 'com.android.support:recyclerview-v7:28.0.0'
    implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'
    implementation 'io.reactivex.rxjava2:rxjava:2.2.6'
    implementation 'com.squareup.retrofit2:adapter-rxjava2:2.4.0'
    implementation 'com.squareup.retrofit2:converter-gson:2.2.0'
    implementation 'com.squareup.retrofit2:retrofit:2.2.0'
    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'
    ...
}

dependencies {
    ...
    classpath 'com.jakewharton:butterknife-gradle-plugin:9.0.0'
    classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'

    
    
}

天氣數據網絡請求分為兩塊,一塊是天氣和圖標的請求,使用RxJava的flatMap操作符將兩者連在一起:首先請求天氣數據,得到數據後使用flatMap操作符,取出數據中的天氣類型進行第二次網絡請求,最後在主線程中處理數據。第二塊是搜索城市返回城市代碼,一個簡單的Retrofit + RxJava就可實現。在https://www.sojson.com/blog/305.html中,有天氣接口成功返回值的Json代碼示例,去掉中間的注釋,只要Json數據。因為使用的Gson,首先準備天氣數據的Bean類。新建WeatherBean.java,將光標放到類的括號中,右鍵選擇Generate,選擇GsonFormat,把Json代碼粘進去,ok,會在類中自動生成對應的代碼,看一眼與數據是對應的。寫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 {
    
    @GET("{city_code}")
    Observable<WeatherBean> 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())
            .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
            .build();
    final WeatherService weatherService = retrofit.create(WeatherService.class);

    
    weatherService.getCall(cityId)
            .subscribeOn(Schedulers.io())
            .flatMap((Function<WeatherBean, Observable<WeatherBean>>) weatherBean -> {
                ...
                for (WeatherBean.DataBean.ForecastBean forecastBean : weatherBean.getData().getForecast()) {
                    ...

                    
                    String url = "https://cdn.heweather.com/cond_icon/" + preferences.getString(forecastBean.getType(), "未知");

                    
                    FutureTarget<Bitmap> target = Glide.with(getApplicationContext())
                            .asBitmap()
                            .load(url)
                            .submit();
                    final Bitmap bitmap = target.get();
                    dataArrayList.add(new MyCurveView.WeatherData(low, high, date, type, bitmap));
                }
                ...

                
                return Observable.fromArray(weatherBean);
            })
            .observeOn(AndroidSchedulers.mainThread())
            .subscribe(new Observer<WeatherBean>() {

                @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<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() {
        @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<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++) {
        
        dataArray.get(i).setHighTemp((innerData.get(i).getHighTemp() - averageHigh) / (max - 0));
        
        dataArray.get(i).setLowTemp((averageLow - innerData.get(i).getLowTemp()) / (min - 0));
    }
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int widthSize = MeasureSpec.getSize(widthMeasureSpec);
    int heightSize = MeasureSpec.getSize(heightMeasureSpec);
    measureWidth = widthSize;
    measureHeight = heightSize;
    
    mTempWidth = measureWidth / 6;
    setMeasuredDimension(widthSize, heightSize);
}

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

@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();
                
                if (isFling) {
                    removeCallbacks(mScrollRunnable);
                    isFling = false;
                }
                break;

            case MotionEvent.ACTION_MOVE:
                float currX = event.getX();
                mStartX += currX - lastX;
                
                
                if ((currX - lastX) > 0) {
                    if (mStartX > 0) {
                        mStartX = 0;
                    }
                } else {
                    if (-mStartX > getMoveLength()) {
                        mStartX = -getMoveLength();
                    }
                }
                lastX = currX;
                break;

            case MotionEvent.ACTION_UP:
                
                mVelocityTracker.computeCurrentVelocity(1000);
                
                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;
    }

城市搜索使用單獨一個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) {

        }
    }
}

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">

    <data>

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

    </data>

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

        <EditText
            android: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())
            .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
            .build();

    CityService cityService = retrofit.create(CityService.class);
    cityService.getCall(s.toString(), KEY)
            .subscribeOn(Schedulers.io())
            .observeOn(AndroidSchedulers.mainThread())
            .subscribe(new Observer<CityBean>() {
                @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<String> citys;

    private Context mContext;

    private OnItemClick mOnItemClick;

    CitysAdapter(ArrayList<String> citys, Context parentContext) {
        this.citys = citys;
        mContext = parentContext;
    }

    void setOnItemClick(OnItemClick onItemClick) {
        mOnItemClick = onItemClick;
    }

    interface OnItemClick {
        
        void onClick(String city, int position);
    }

    static class ViewHolder extends RecyclerView.ViewHolder {

        private LinearLayout layout;

        private TextView textView;

        ViewHolder(@NonNull View itemView) {
            super(itemView);
            layout = itemView.findViewById(R.id.city_layout);
            textView = itemView.findViewById(R.id.city);
        }
    }

    @NonNull
    @Override
    public ViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) {
        if (mContext == null) {
            mContext = viewGroup.getContext();
        }
        View view = LayoutInflater.from(mContext).inflate(R.layout.city_item, viewGroup, false);
        return new ViewHolder(view);
    }

    @Override
    public void onBindViewHolder(@NonNull ViewHolder viewHolder, int position) {
        viewHolder.textView.setText(citys.get(position));
        viewHolder.layout.setOnClickListener(v -> mOnItemClick.onClick(citys.get(position), position));
    }

    @Override
    public int getItemCount() {
        return citys.size();
    }

}

添加CityMessageEvent.java,供EventBus使用。

public class CityMessageEvent {

    private String name;

    private String cityId;

    
    ...

}

在CityActivity中實現上面的onClick()方法。

private CityMessageEvent messageEvent;

@Override
public void onClick(String city, int position) {
    if (mCityBean != null) {
        messageEvent.setName(city);
        messageEvent.setCityId(mCityBean.getHeWeather6().get(0).getBasic().get(position).getCid().replace("CN", ""));
        
        EventBus.getDefault().postSticky(messageEvent);
    }
    finish();
}

修改MainActivity,添加onMessageEvent()方法,用來接收EventBus發出的event。接收到數據後,再次請求網絡數據。

@Subscribe(threadMode = ThreadMode.MAIN, sticky = true)
public void onMessageEvent(CityMessageEvent messageEvent) {
    requestWeather(messageEvent.getCityId());
}

最後附上源碼地址,有什麼疑問及意見歡迎大家指出,後續有時間會對代碼進行改進。要運行的話,麻煩在和風天氣官網註冊一下,自己新建一個工程,使用自己的Key,謝謝~~

項目地址:GitHub

https://github.com/songkai0825/SimpleWeather

版權聲明:本文為CSDN博主「Kai_0825」的原創文章,遵循 CC 4.0 BY-SA 版權協議,轉載請附上原文出處連結及本聲明。https://blog.csdn.net/songkai0825/article/details/95257837


 推薦↓↓↓ 

涵蓋:程式設計師大咖、源碼共讀、程式設計師共讀、數據結構與算法、黑客技術和網絡安全、大數據科技、編程前端、Java、Python、Web編程開發、Android、iOS開發、Linux、資料庫研發、幽默程式設計師等。

相關焦點

  • android studio布局嵌套_android studio相對布局和線性布局嵌套...
    利用android studio LinearLayout線性布局設計製作簡易的計算器詳細版【精選收藏】Android Studio簡介Android Studio 是谷歌推出的一個Android集成開發工具
  • Android TV開發總結(一)構建一個TV app前要知道的事兒
    app上檢查電視設備如果您正在構建一個app運行在TV設備和其他設備,你也許需要去check你的app運行在什麼樣的設備上且可能將在你的app做何種操作。 例如,如果您有一個app被Intent啟動,你的應用應當被檢查設備屬性去確定是否能啟動在TV下的activity或者是在手機上的activity.
  • android app殺死啟動專題及常見問題 - CSDN
    一般是用戶按了home鍵回到桌面,或者返回鍵沒有殺進程,或者app本身做了進程重啟的機制。冷啟動通常會發生在一下兩種情況:設備啟動以來首次啟動應用程式系統殺死應用程式之後再次啟動應用程式在冷啟動的最開始,系統需要負責做三件事:加載以及啟動appapp啟動之後立刻顯示一個空白的預覽窗口創建app進程
  • Android 約束布局(ConstraintLayout)詳解
    創建布局接下來,我們創建一個布局,根布局就用ConstraintLayout:<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com
  • Android TV開發簡介
    1.新建一個AndroidTV工程Android TV工程使用和Android Phone工程相同的文件結構,一樣可以使用Android Studio+Gradle的方式進行編輯和構建。(這種相似性意味著你可以輕微修改現有的Phone端app以使其可以在TV端運行。)
  • Android仿蝦米音樂新版Tab標籤頁
    ><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width
  • Android中LayoutAnimation的分析(一)
    GridLayoutAnimationController 是繼承於 LayoutAnimationController,功能都差不多,只是比 LayoutAnimationController 多了幾個功能參數;現在我們先寫 demo 看一下效果,然後再分析一下相關的源碼;1、LayoutAnimationController demo1、1 xml 的方式進行動畫(1)在 app
  • Android Notes|BottomNavigationView 愛上 Lottie
    >    <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"        xmlns:app="http://schemas.android.com/apk/res-auto"        xmlns
  • Android 樣式系統 | 主題背景屬性
    通過使用主題背景屬性,我們可以將語義顏色的聲明從提供它們的值中區分開來,而且讓使用方更清楚地了解到顏色會隨主題背景而變化 (因為它們使用 ?attr/ 語法)。將顏色聲明保持為字面值,您就可以自定義應用使用的顏色調色板,並在主題背景級別修改它們,這會讓 color.xml 較小且易維護。這種方法的額外好處是,布局/樣式引用這些顏色時復用性變得更高。
  • Android 轉場動畫
    在android.transition包下提供關於transitionAnimation的過渡框架, Transiton框架是在api19引入, 但是轉場動畫卻是在api21引入.:transitionName="button"<Button        android:id="@+id/button2"        android:layout_width="wrap_content"        android:layout_height="wrap_content"        android:text="
  • Android官方架構組件Navigation:大巧不工的Fragment管理框架
    ><navigation xmlns:android="http://schemas.android.com/apk/res/android"    xmlns:app="http://schemas.android.com/apk/res-auto"    xmlns:tools="http://schemas.android.com/tools"
  • Android人臉識別app——基於Face++,MVP+Retofit+RxJava+Dagger高度解耦
    作者:reggie1996連結:https://www.jianshu.com/p/920b9c525d2f前言最近公司項目比較空,花了點時間寫了個人臉識別的app面部識別主界面        <provider            android:name="android.support.v4.content.FileProvider"            android:authorities="com.chaochaowu.facedetect.provider"            android:exported
  • Android TV開發總結(六)構建一個TV app的直播節目實例
    今天將介紹構建一個TV app的直播節目實例,此實例上傳到Github: https://github.com/hejunlin2013/LivePlayback 喜歡可以star。下方"閱讀原文"可直接到該實例的github, 本文Agenda如下:先看下效果圖:主界面:
  • 利用Python開發App
    玩玩,無奈對java不夠熟悉,之前也沒有開發app的經驗,因此一直耽擱了。簡而言之,這是一個python桌面程序開發框架(類似wxpython等模塊),強大的是kivy支持linux、mac、windows、android、ios平臺,這也是為什麼開發app需要用到這個模塊。
  • 夜神模擬器模擬APP+Appium+mitmdump數據抓取
    Original error: Could not find 'apksigner.jar' in ["/ApplicationsxAppPlayer.app/Contents/MacOS/platform-tools/apksigner.jar","/ApplicationsxAppPlayer.app/Contents/MacOS/emulator/apksigner.jar","/ApplicationsxAppPlayer.app
  • appium+python自動化36-android7.0連不上的問題
    前言由於最近很多android手機升級到7.0系統了,有些小夥伴的appium版本用的還是1.4版本,在運行android7.0的app自動化時候遇到無法啟動問題
  • 天氣預報app官網下載
    墨跡天氣是一款提供穿衣、洗車、紫外線、感冒、運動、旅遊等生活指數,是您日常生活、出行旅遊的必備天氣預報APP,此外app還提供15天天氣預報,5天空氣品質預報,實時空氣品質及空氣品質等級預報。特殊天氣提前發送預警信息,幫助用戶更好做出生活決策,從容應對各類天氣狀況。
  • Android Gradle 常用使用場景實現方式的總結
    修改 Manifest 文件中友盟統計的渠道名為引用變量(變量名自取):<meta-data android:name="${UMENG_CHANNEL_VALUE}" android:value="Channel_ID" />然後在 build.gradle 文件 productFlavors 配置項中添加渠道名
  • 我感覺我學了一個假的Android...
    你作為一隻老鳥,肯定立馬腦子裡閃過:我知道你這文章寫啥了,又要在Activity#onCreate,去搞個線程執行TextView#setText,然後發現更新成功了,是不是?這多年以前我就看過這樣的文章,ViewRootImpl還沒創建而已。看你們這麼強,我這個文章沒法寫下去了...
  • 利用Python開發App實戰
    (點擊上方公眾號,可快速關注)我很早之前就想開發一款app玩玩,無奈對java不夠熟悉,之前也沒有開發app的經驗,因此一直耽擱了。說在前面的話python語言雖然很萬能,但用它來開發app還是顯得有點不對路,因此用python開發的app應當是作為編碼練習、或者自娛自樂所用,加上目前這方面的模塊還不是特別成熟,bug比較多,總而言之,勸君莫輕入。