Administrator
2021-11-02 313c1ad8510711357827ce879b449dcb770bce9a
titlebar base
登录页UI
21 files added
13 files modified
1061 ■■■■■ changed files
app/src/main/AndroidManifest.xml 3 ●●●●● patch | view | raw | blame | history
app/src/main/java/com/duqing/missions/MainActivity.java 11 ●●●●● patch | view | raw | blame | history
app/src/main/java/com/duqing/missions/base/BaseActivity.java 70 ●●●●● patch | view | raw | blame | history
app/src/main/java/com/duqing/missions/base/BaseFragment.java 8 ●●●● patch | view | raw | blame | history
app/src/main/java/com/duqing/missions/base/BaseTitleBarActivity.java 57 ●●●●● patch | view | raw | blame | history
app/src/main/java/com/duqing/missions/ui/login/data/LoginDataSource.java 40 ●●●●● patch | view | raw | blame | history
app/src/main/java/com/duqing/missions/ui/login/data/LoginRepository.java 53 ●●●●● patch | view | raw | blame | history
app/src/main/java/com/duqing/missions/ui/login/data/model/LoggedInUser.java 23 ●●●●● patch | view | raw | blame | history
app/src/main/java/com/duqing/missions/ui/login/view/LoggedInUserView.java 17 ●●●●● patch | view | raw | blame | history
app/src/main/java/com/duqing/missions/ui/login/view/LoginActivity.java 153 ●●●●● patch | view | raw | blame | history
app/src/main/java/com/duqing/missions/ui/login/view/LoginFormState.java 40 ●●●●● patch | view | raw | blame | history
app/src/main/java/com/duqing/missions/ui/login/view/LoginResult.java 31 ●●●●● patch | view | raw | blame | history
app/src/main/java/com/duqing/missions/ui/login/view/LoginViewModel.java 89 ●●●●● patch | view | raw | blame | history
app/src/main/java/com/duqing/missions/ui/login/view/LoginViewModelFactory.java 27 ●●●●● patch | view | raw | blame | history
app/src/main/java/com/duqing/missions/ui/main/dashboard/DashboardFragment.java 4 ●●● patch | view | raw | blame | history
app/src/main/java/com/duqing/missions/ui/main/home/HomeFragment.java 6 ●●●●● patch | view | raw | blame | history
app/src/main/java/com/duqing/missions/ui/main/home/HomeViewModel.java 2 ●●● patch | view | raw | blame | history
app/src/main/java/com/duqing/missions/ui/main/notifications/NotificationsFragment.java 25 ●●●● patch | view | raw | blame | history
app/src/main/java/com/duqing/missions/widgets/ClearEditText.java 194 ●●●●● patch | view | raw | blame | history
app/src/main/res/layout/activity_login.xml 161 ●●●●● patch | view | raw | blame | history
app/src/main/res/mipmap-hdpi/icon_back_black.png patch | view | raw | blame | history
app/src/main/res/mipmap-hdpi/icon_white_back.png patch | view | raw | blame | history
app/src/main/res/mipmap-xhdpi/icon_back_black.png patch | view | raw | blame | history
app/src/main/res/mipmap-xhdpi/icon_white_back.png patch | view | raw | blame | history
app/src/main/res/mipmap-xxhdpi/icon_back_black.png patch | view | raw | blame | history
app/src/main/res/mipmap-xxhdpi/icon_delete.png patch | view | raw | blame | history
app/src/main/res/mipmap-xxhdpi/icon_white_back.png patch | view | raw | blame | history
app/src/main/res/mipmap-xxxhdpi/icon_back_black.png patch | view | raw | blame | history
app/src/main/res/mipmap-xxxhdpi/icon_white_back.png patch | view | raw | blame | history
app/src/main/res/values-night/themes.xml 10 ●●●● patch | view | raw | blame | history
app/src/main/res/values/colors.xml 5 ●●●● patch | view | raw | blame | history
app/src/main/res/values/strings.xml 8 ●●●●● patch | view | raw | blame | history
app/src/main/res/values/styles.xml 12 ●●●●● patch | view | raw | blame | history
app/src/main/res/values/themes.xml 12 ●●●● patch | view | raw | blame | history
app/src/main/AndroidManifest.xml
@@ -1,5 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    tools:ignore="ProtectedPermissions"
    package="com.duqing.missions" >
    <uses-permission android:name="android.permission.READ_PRIVILEGED_PHONE_STATE" />
@@ -20,6 +22,7 @@
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <activity android:name=".ui.login.view.LoginActivity" />
    </application>
</manifest>
app/src/main/java/com/duqing/missions/MainActivity.java
@@ -1,10 +1,7 @@
package com.duqing.missions;
import android.os.Bundle;
import androidx.navigation.NavController;
import androidx.navigation.Navigation;
import androidx.navigation.ui.AppBarConfiguration;
import androidx.navigation.ui.NavigationUI;
import com.duqing.missions.base.BaseActivity;
@@ -14,16 +11,10 @@
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
    public void initViews() {
        // Passing each menu ID as a set of Ids because each
        // menu should be considered as top level destinations.
        AppBarConfiguration appBarConfiguration = new AppBarConfiguration.Builder(
                R.id.navigation_home, R.id.navigation_dashboard, R.id.navigation_notifications)
                .build();
        NavController navController = Navigation.findNavController(this, R.id.nav_host_fragment_activity_main);
        NavigationUI.setupActionBarWithNavController(this, navController, appBarConfiguration);
        NavigationUI.setupWithNavController(binding.navView, navController);
    }
app/src/main/java/com/duqing/missions/base/BaseActivity.java
@@ -14,12 +14,16 @@
import android.provider.Settings;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.WindowManager;
import android.view.inputmethod.InputMethodManager;
import android.widget.EditText;
import android.widget.Toast;
import androidx.annotation.ColorRes;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.content.FileProvider;
import androidx.viewbinding.ViewBinding;
@@ -114,7 +118,10 @@
        } catch (Exception e) {
        }
        TAG = this.getClass().getSimpleName();
        initViews();
    }
    public abstract void initViews();
    public void setStatusBarTransparent(boolean isBlack){
@@ -164,6 +171,62 @@
    }
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {  //把操作放在用户点击的时候
            View v = getCurrentFocus();      //得到当前页面的焦点,ps:有输入框的页面焦点一般会被输入框占据
            if (isShouldHideKeyboard(v, ev)) { //判断用户点击的是否是输入框以外的区域
                hideSoftKeyboard ();   //收起键盘
            }
        }
        return super.dispatchTouchEvent(ev);
    }
    /**
     * 根据EditText所在坐标和用户点击的坐标相对比,来判断是否隐藏键盘,因为当用户点击EditText时则不能隐藏
     *
     * @param v
     * @param event
     * @return
     */
    private boolean isShouldHideKeyboard(View v, MotionEvent event) {
        if (v != null && (v instanceof EditText)) {  //判断得到的焦点控件是否包含EditText
            int[] l = {0, 0};
            v.getLocationInWindow(l);
            int left = l[0],    //得到输入框在屏幕中上下左右的位置
                    top = l[1],
                    bottom = top + v.getHeight(),
                    right = left + v.getWidth();
            if (event.getX() > left && event.getX() < right
                    && event.getY() > top && event.getY() < bottom) {
                // 点击位置如果是EditText的区域,忽略它,不收起键盘。
                return false;
            } else {
                return true;
            }
        }
        // 如果焦点不是EditText则忽略
        return false;
    }
    /**
     * 判断软键盘输入法是否弹出
     */
    public boolean isKeyboardVisbility(Context context, View v) {
        InputMethodManager imm = (InputMethodManager) context.getSystemService(context.INPUT_METHOD_SERVICE);
        if (imm.hideSoftInputFromWindow(v.getWindowToken(), 0)) {
            imm.showSoftInput(v, 0);
            return true;//键盘显示中
        } else {
            return false;//键盘未显示
        }
    }
    protected void hideSoftKeyboard() {
        if (getWindow().getAttributes().softInputMode != WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN) {
            if (getCurrentFocus() != null)
                ((InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE)).hideSoftInputFromWindow(getCurrentFocus().getWindowToken(),
                        InputMethodManager.HIDE_NOT_ALWAYS);
        }
    }
    /**
     * 状态栏高度
     * @return
@@ -193,6 +256,13 @@
            //quitApp();
        }
    }
    public void showToast(String message){
        Toast.makeText(this,message,Toast.LENGTH_SHORT).show();
    }
    public void showToast(@StringRes int msg){
        showToast(getString(msg));
    }
    ApkUpGradeResult.AppInfo apkUpGrade;
    ProgressDialog progressDialog ;
app/src/main/java/com/duqing/missions/base/BaseFragment.java
@@ -15,16 +15,16 @@
/**
 * Created by Administrator on 2021/10/28 0028.
 */
public abstract class BaseFragment<A extends BaseActivity,B extends ViewBinding> extends Fragment {
public abstract class BaseFragment<B extends ViewBinding> extends Fragment {
    protected  A activity;
    protected  BaseActivity activity;
    protected  B binding;
    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) {
        // get genericity "B"
        Class<B> entityClass = (Class<B>) ((ParameterizedType) this.getClass().getGenericSuperclass()).getActualTypeArguments()[1];
        Class<B> entityClass = (Class<B>) ((ParameterizedType) this.getClass().getGenericSuperclass()).getActualTypeArguments()[0];
        try {
            Method method = entityClass.getMethod("inflate", LayoutInflater.class,ViewGroup.class,boolean.class);//get method from name "inflate";
            binding = (B) method.invoke(entityClass,inflater,container,false);//execute method to create a objct of viewbind;
@@ -37,7 +37,7 @@
    @Override
    public void onActivityCreated(@Nullable Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        activity = (A) getActivity();
        activity = (BaseActivity) getActivity();
        initViews();
    }
app/src/main/java/com/duqing/missions/base/BaseTitleBarActivity.java
New file
@@ -0,0 +1,57 @@
package com.duqing.missions.base;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.view.ViewGroup;
import androidx.annotation.Nullable;
import androidx.viewbinding.ViewBinding;
import com.duqing.missions.widgets.TitleBarView;
/**
 * Created by Administrator on 2021/11/2 0002.
 */
public abstract class BaseTitleBarActivity<B extends ViewBinding> extends BaseActivity<B> {
    TitleBarView titleBarView;
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        try {
            titleBarView = (TitleBarView) binding.getClass().getDeclaredField("titleBar").get(binding);
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        }
    }
    protected void setTitle(String text){
        titleBarView.setTitleText(text);
    }
    protected void onTitleLeftClick(){
        onBackKeyDown();
    }
    protected void setTitleRight(String text){
        titleBarView.setRightText(text);
        titleBarView.setRightDra(null);
    }
    protected void setTitleRight(Drawable drawable){
        titleBarView.setRightText(null);
        titleBarView.setRightDra(drawable);
    }
    @Override
    public void setStatusBarTransparent(boolean isBlack) {
        super.setStatusBarTransparent(isBlack);
        final ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams) titleBarView.getLayoutParams();
        layoutParams.topMargin = layoutParams.topMargin+getStatusBarHeight();
        titleBarView.setLayoutParams(layoutParams);
    }
}
app/src/main/java/com/duqing/missions/ui/login/data/LoginDataSource.java
New file
@@ -0,0 +1,40 @@
package com.duqing.missions.ui.login.data;
import com.duqing.missions.ui.login.data.model.LoggedInUser;
import io.reactivex.Observable;
import io.reactivex.ObservableEmitter;
import io.reactivex.ObservableOnSubscribe;
/**
 * Class that handles authentication w/ login credentials and retrieves user information.
 */
public class LoginDataSource {
    public Observable<LoggedInUser> login(String username, String password) {
        final Observable<LoggedInUser> observable = Observable.create(new ObservableOnSubscribe<LoggedInUser>() {
            @Override
            public void subscribe(ObservableEmitter<LoggedInUser> e) throws Exception {
                LoggedInUser fakeUser = new LoggedInUser( java.util.UUID.randomUUID().toString(), "Jane Doe");
                e.onNext(fakeUser);
            }
        });
        return  observable;
    }
    public Observable<LoggedInUser> loginByCode(String phone, String verifyCode){
        final Observable<LoggedInUser> observable = Observable.create(new ObservableOnSubscribe<LoggedInUser>() {
            @Override
            public void subscribe(ObservableEmitter<LoggedInUser> e) throws Exception {
                LoggedInUser fakeUser = new LoggedInUser( java.util.UUID.randomUUID().toString(), "Jane Doe");
                e.onNext(fakeUser);
            }
        });
        return  observable;
    }
    public void logout() {
        // TODO: revoke authentication
    }
}
app/src/main/java/com/duqing/missions/ui/login/data/LoginRepository.java
New file
@@ -0,0 +1,53 @@
package com.duqing.missions.ui.login.data;
import com.duqing.missions.ui.login.data.model.LoggedInUser;
import io.reactivex.Observable;
/**
 * Class that requests authentication and user information from the remote data source and
 * maintains an in-memory cache of login status and user credentials information.
 */
public class LoginRepository {
    private static volatile LoginRepository instance;
    private LoginDataSource dataSource;
    // If user credentials will be cached in local storage, it is recommended it be encrypted
    // @see https://developer.android.com/training/articles/keystore
    private LoggedInUser user = null;
    // private constructor : singleton access
    private LoginRepository(LoginDataSource dataSource) {
        this.dataSource = dataSource;
    }
    public static LoginRepository getInstance(LoginDataSource dataSource) {
        if (instance == null) {
            instance = new LoginRepository(dataSource);
        }
        return instance;
    }
    public boolean isLoggedIn() {
        return user != null;
    }
    public void logout() {
        user = null;
        dataSource.logout();
    }
    private void setLoggedInUser(LoggedInUser user) {
        this.user = user;
        // If user credentials will be cached in local storage, it is recommended it be encrypted
        // @see https://developer.android.com/training/articles/keystore
    }
    public Observable<LoggedInUser> login(String username, String password) {
        // handle login
        return dataSource.login(username, password);
    }
}
app/src/main/java/com/duqing/missions/ui/login/data/model/LoggedInUser.java
New file
@@ -0,0 +1,23 @@
package com.duqing.missions.ui.login.data.model;
/**
 * Data class that captures user information for logged in users retrieved from LoginRepository
 */
public class LoggedInUser {
    private String userId;
    private String displayName;
    public LoggedInUser(String userId, String displayName) {
        this.userId = userId;
        this.displayName = displayName;
    }
    public String getUserId() {
        return userId;
    }
    public String getDisplayName() {
        return displayName;
    }
}
app/src/main/java/com/duqing/missions/ui/login/view/LoggedInUserView.java
New file
@@ -0,0 +1,17 @@
package com.duqing.missions.ui.login.view;
/**
 * Class exposing authenticated user details to the UI.
 */
class LoggedInUserView {
    private String displayName;
    //... other data fields that may be accessible to the UI
    LoggedInUserView(String displayName) {
        this.displayName = displayName;
    }
    String getDisplayName() {
        return displayName;
    }
}
app/src/main/java/com/duqing/missions/ui/login/view/LoginActivity.java
New file
@@ -0,0 +1,153 @@
package com.duqing.missions.ui.login.view;
import android.app.Activity;
import android.graphics.Typeface;
import android.text.Editable;
import android.text.TextWatcher;
import android.util.TypedValue;
import android.view.KeyEvent;
import android.view.View;
import android.view.inputmethod.EditorInfo;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.lifecycle.Observer;
import androidx.lifecycle.ViewModelProvider;
import com.duqing.missions.R;
import com.duqing.missions.base.BaseTitleBarActivity;
import com.duqing.missions.databinding.ActivityLoginBinding;
public class LoginActivity extends BaseTitleBarActivity<ActivityLoginBinding> {
    private LoginViewModel loginViewModel;
    @Override
    public void initViews() {
        loginViewModel = new ViewModelProvider(this, new LoginViewModelFactory()).get(LoginViewModel.class);
        final EditText phoneEdit = binding.editPhone;
        final EditText passwordEditText = binding.editPassword;
        final Button loginButton = binding.login;
        loginViewModel.getLoginFormState().observe(this, new Observer<LoginFormState>() {
            @Override
            public void onChanged(@Nullable LoginFormState loginFormState) {
                if (loginFormState == null) {
                    return;
                }
                loginButton.setEnabled(loginFormState.isDataValid());
                if (loginFormState.getUsernameError() != null) {
                    phoneEdit.setError(getString(loginFormState.getUsernameError()));
                }
                if (loginFormState.getPasswordError() != null) {
                    passwordEditText.setError(getString(loginFormState.getPasswordError()));
                }
            }
        });
        loginViewModel.getLoginResult().observe(this, new Observer<LoginResult>() {
            @Override
            public void onChanged(@Nullable LoginResult loginResult) {
                if (loginResult == null) {
                    return;
                }
                if (loginResult.getError() != null) {
                    showLoginFailed(loginResult.getError());
                }
                if (loginResult.getSuccess() != null) {
                    updateUiWithUser(loginResult.getSuccess());
                }
                setResult(Activity.RESULT_OK);
                //Complete and destroy login activity once successful
                finish();
            }
        });
        binding.txtPasswordTitle.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                binding.containerVerify.setVisibility(View.GONE);
                binding.containerPassword.setVisibility(View.VISIBLE);
                checkedStyle(binding.txtPasswordTitle);
                unCheckStyle(binding.txtVerifyTitle);
            }
        });
        binding.txtVerifyTitle.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                binding.containerPassword.setVisibility(View.GONE);
                binding.containerVerify.setVisibility(View.VISIBLE);
                checkedStyle(binding.txtVerifyTitle);
                unCheckStyle(binding.txtPasswordTitle);
            }
        });
        TextWatcher afterTextChangedListener = new TextWatcher() {
            @Override
            public void beforeTextChanged(CharSequence s, int start, int count, int after) {
                // ignore
            }
            @Override
            public void onTextChanged(CharSequence s, int start, int before, int count) {
                // ignore
            }
            @Override
            public void afterTextChanged(Editable s) {
                loginViewModel.loginDataChanged(phoneEdit.getText().toString(),
                        passwordEditText.getText().toString());
            }
        };
        phoneEdit.addTextChangedListener(afterTextChangedListener);
        passwordEditText.addTextChangedListener(afterTextChangedListener);
        passwordEditText.setOnEditorActionListener(new TextView.OnEditorActionListener() {
            @Override
            public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
                if (actionId == EditorInfo.IME_ACTION_DONE) {
                    loginViewModel.login(phoneEdit.getText().toString(),
                            passwordEditText.getText().toString());
                }
                return false;
            }
        });
        loginButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                loginViewModel.login(phoneEdit.getText().toString(),
                        passwordEditText.getText().toString());
            }
        });
    }
    public void checkedStyle(TextView textView){
        textView.setTextSize(TypedValue.COMPLEX_UNIT_SP,17);
        textView.setTypeface(Typeface.defaultFromStyle(Typeface.BOLD));
    }
    public void unCheckStyle(TextView textView){
        textView.setTextSize(TypedValue.COMPLEX_UNIT_SP,14);
        textView.setTypeface(Typeface.defaultFromStyle(Typeface.NORMAL));
    }
    private void updateUiWithUser(LoggedInUserView model) {
        String welcome = getString(R.string.welcome) + model.getDisplayName();
        // TODO : initiate successful logged in experience
        Toast.makeText(getApplicationContext(), welcome, Toast.LENGTH_LONG).show();
    }
    private void showLoginFailed(@StringRes Integer errorString) {
        Toast.makeText(getApplicationContext(), errorString, Toast.LENGTH_SHORT).show();
    }
}
app/src/main/java/com/duqing/missions/ui/login/view/LoginFormState.java
New file
@@ -0,0 +1,40 @@
package com.duqing.missions.ui.login.view;
import androidx.annotation.Nullable;
/**
 * Data validation state of the login form.
 */
class LoginFormState {
    @Nullable
    private Integer usernameError;
    @Nullable
    private Integer passwordError;
    private boolean isDataValid;
    LoginFormState(@Nullable Integer usernameError, @Nullable Integer passwordError) {
        this.usernameError = usernameError;
        this.passwordError = passwordError;
        this.isDataValid = false;
    }
    LoginFormState(boolean isDataValid) {
        this.usernameError = null;
        this.passwordError = null;
        this.isDataValid = isDataValid;
    }
    @Nullable
    Integer getUsernameError() {
        return usernameError;
    }
    @Nullable
    Integer getPasswordError() {
        return passwordError;
    }
    boolean isDataValid() {
        return isDataValid;
    }
}
app/src/main/java/com/duqing/missions/ui/login/view/LoginResult.java
New file
@@ -0,0 +1,31 @@
package com.duqing.missions.ui.login.view;
import androidx.annotation.Nullable;
/**
 * Authentication result : success (user details) or error message.
 */
class LoginResult {
    @Nullable
    private LoggedInUserView success;
    @Nullable
    private Integer error;
    LoginResult(@Nullable Integer error) {
        this.error = error;
    }
    LoginResult(@Nullable LoggedInUserView success) {
        this.success = success;
    }
    @Nullable
    LoggedInUserView getSuccess() {
        return success;
    }
    @Nullable
    Integer getError() {
        return error;
    }
}
app/src/main/java/com/duqing/missions/ui/login/view/LoginViewModel.java
New file
@@ -0,0 +1,89 @@
package com.duqing.missions.ui.login.view;
import android.util.Patterns;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import com.duqing.missions.R;
import com.duqing.missions.ui.login.data.LoginRepository;
import com.duqing.missions.ui.login.data.model.LoggedInUser;
import io.reactivex.Observable;
import io.reactivex.disposables.Disposable;
import io.reactivex.functions.Consumer;
import io.reactivex.observers.DisposableObserver;
public class LoginViewModel extends ViewModel {
    private MutableLiveData<LoginFormState> loginFormState = new MutableLiveData<>();
    private MutableLiveData<LoginResult> loginResult = new MutableLiveData<>();
    private LoginRepository loginRepository;
    LoginViewModel(LoginRepository loginRepository) {
        this.loginRepository = loginRepository;
    }
    LiveData<LoginFormState> getLoginFormState() {
        return loginFormState;
    }
    LiveData<LoginResult> getLoginResult() {
        return loginResult;
    }
    public void login(String username, String password) {
        // can be launched in a separate asynchronous job
        Observable<LoggedInUser> result = loginRepository.login(username, password);
        result.doOnSubscribe(new Consumer<Disposable>() {
            @Override
            public void accept(Disposable disposable) throws Exception {
            }
        }).subscribe(new DisposableObserver<LoggedInUser>(){
            @Override
            public void onNext(LoggedInUser value) {
                loginResult.setValue(new LoginResult((new LoggedInUserView(value.getDisplayName()))));
            }
            @Override
            public void onError(Throwable e) {
                loginResult.setValue(new LoginResult(R.string.login_failed));
            }
            @Override
            public void onComplete() {
            }
        });
    }
    public void loginDataChanged(String username, String password) {
        if (!isUserNameValid(username)) {
            loginFormState.setValue(new LoginFormState(R.string.invalid_username, null));
        } else if (!isPasswordValid(password)) {
            loginFormState.setValue(new LoginFormState(null, R.string.invalid_password));
        } else {
            loginFormState.setValue(new LoginFormState(true));
        }
    }
    // A placeholder username validation check
    private boolean isUserNameValid(String username) {
        if (username == null) {
            return false;
        }
        if (username.contains("@")) {
            return Patterns.EMAIL_ADDRESS.matcher(username).matches();
        } else {
            return !username.trim().isEmpty();
        }
    }
    // A placeholder password validation check
    private boolean isPasswordValid(String password) {
        return password != null && password.trim().length() > 5;
    }
}
app/src/main/java/com/duqing/missions/ui/login/view/LoginViewModelFactory.java
New file
@@ -0,0 +1,27 @@
package com.duqing.missions.ui.login.view;
import androidx.annotation.NonNull;
import androidx.lifecycle.ViewModel;
import androidx.lifecycle.ViewModelProvider;
import com.duqing.missions.ui.login.data.LoginDataSource;
import com.duqing.missions.ui.login.data.LoginRepository;
/**
 * ViewModel provider factory to instantiate LoginViewModel.
 * Required given LoginViewModel has a non-empty constructor
 */
public class LoginViewModelFactory implements ViewModelProvider.Factory {
    @NonNull
    @Override
    @SuppressWarnings("unchecked")
    public <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
        if (modelClass.isAssignableFrom(LoginViewModel.class)) {
            return (T) new LoginViewModel(LoginRepository.getInstance(new LoginDataSource()));
        } else {
            throw new IllegalArgumentException("Unknown ViewModel class");
        }
    }
}
app/src/main/java/com/duqing/missions/ui/main/dashboard/DashboardFragment.java
@@ -4,14 +4,12 @@
import androidx.lifecycle.Observer;
import androidx.lifecycle.ViewModelProvider;
import com.duqing.missions.MainActivity;
import com.duqing.missions.base.BaseFragment;
import com.duqing.missions.databinding.FragmentDashboardBinding;
public class DashboardFragment extends BaseFragment<MainActivity,FragmentDashboardBinding> {
public class DashboardFragment extends BaseFragment<FragmentDashboardBinding> {
    private DashboardViewModel dashboardViewModel;
    private FragmentDashboardBinding binding;
    @Override
    public void initViews() {
app/src/main/java/com/duqing/missions/ui/main/home/HomeFragment.java
@@ -1,5 +1,6 @@
package com.duqing.missions.ui.main.home;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
@@ -16,9 +17,9 @@
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.duqing.missions.MainActivity;
import com.duqing.missions.base.BaseFragment;
import com.duqing.missions.databinding.FragmentHomeBinding;
import com.duqing.missions.ui.login.view.LoginActivity;
import com.duqing.missions.ui.main.home.adapter.MissionAdapter;
import com.duqing.missions.ui.main.home.adapter.MissionTopAdapter;
import com.duqing.missions.ui.main.home.model.MissionDesc;
@@ -30,7 +31,7 @@
import java.util.List;
public class HomeFragment extends BaseFragment<MainActivity,FragmentHomeBinding> {
public class HomeFragment extends BaseFragment<FragmentHomeBinding> {
    private HomeViewModel homeViewModel;
    final String TAG = "HomeFragment";
@@ -39,6 +40,7 @@
    @Override
    public void initViews() {
        homeViewModel = new ViewModelProvider(this).get(HomeViewModel.class);
        binding.imgSearch.setOnClickListener(v -> startActivity(new Intent(getContext(), LoginActivity.class)));
        final SmartRefreshLayout smartRefresh = binding.smartRefresh;
        smartRefresh.setRefreshHeader(new ClassicsHeader(getContext()));
        smartRefresh.setRefreshFooter(new ClassicsFooter(getContext()));
app/src/main/java/com/duqing/missions/ui/main/home/HomeViewModel.java
@@ -47,7 +47,7 @@
    }
    public void onLoadMore(){
        List<MissionDesc> list = recommendMissions.getValue();
        List<MissionDesc> list = recommendMissions.getValue() == null? new ArrayList<>():recommendMissions.getValue() ;
        list.add(new MissionDesc());
        list.add(new MissionDesc());
        list.add(new MissionDesc());
app/src/main/java/com/duqing/missions/ui/main/notifications/NotificationsFragment.java
@@ -1,39 +1,24 @@
package com.duqing.missions.ui.main.notifications;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.Observer;
import androidx.lifecycle.ViewModelProvider;
import com.duqing.missions.base.BaseFragment;
import com.duqing.missions.databinding.FragmentNotificationsBinding;
public class NotificationsFragment extends Fragment {
public class NotificationsFragment extends BaseFragment<FragmentNotificationsBinding> {
    private NotificationsViewModel notificationsViewModel;
    private FragmentNotificationsBinding binding;
    public View onCreateView(@NonNull LayoutInflater inflater,  ViewGroup container, Bundle savedInstanceState) {
    @Override
    public void initViews() {
        notificationsViewModel = new ViewModelProvider(this).get(NotificationsViewModel.class);
        binding = FragmentNotificationsBinding.inflate(inflater, container, false);
        View root = binding.getRoot();
        notificationsViewModel.getText().observe(getViewLifecycleOwner(), new Observer<String>() {
            @Override
            public void onChanged(@Nullable String s) {
            }
        });
        return root;
    }
    @Override
    public void onDestroyView() {
        super.onDestroyView();
        binding = null;
    }
}
app/src/main/java/com/duqing/missions/widgets/ClearEditText.java
New file
@@ -0,0 +1,194 @@
package com.duqing.missions.widgets;
import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.text.Editable;
import android.text.TextWatcher;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.animation.Animation;
import android.view.animation.CycleInterpolator;
import android.view.animation.TranslateAnimation;
import android.widget.EditText;
import com.duqing.missions.R;
/**
 * My father is Object, ites purpose of
 *
 * @purpose Created by Runt (qingingrunt2010@qq.com) on 2020-2-25.
 */
@SuppressLint("AppCompatCustomView")
public class ClearEditText extends EditText implements View.OnFocusChangeListener,
        TextWatcher {
    /**
     * 删除按钮的引用
     */
    private Drawable mClearDrawable;
    /**
     * 控件是否有焦点
     */
    private boolean hasFoucs;
    /**
     * 是否可清除内容
     */
    private boolean isClearable = true;
    /**
     * @param context
     * @Description TODO
     */
    public ClearEditText(Context context) {
        this(context, null);
    }
    /**
     * @param context
     * @param attrs
     * @Description TODO
     */
    public ClearEditText(Context context, AttributeSet attrs) {
        // 这里构造方法也很重要,不加这个很多属性不能再XML里面定义
        this(context, attrs, android.R.attr.editTextStyle);
    }
    /**
     * @param context
     * @param attrs
     * @param defStyle
     * @Description TODO
     */
    public ClearEditText(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        init();
    }
    private void init() {
        // 获取EditText的DrawableRight,假如没有设置我们就使用默认的图片
        mClearDrawable = getCompoundDrawables()[2];
        if (mClearDrawable == null) {
            // throw new
            // NullPointerException("You can add drawableRight attribute in XML");
            mClearDrawable = getResources().getDrawable(R.mipmap.icon_delete);
        }
        mClearDrawable.setBounds(0, 0, mClearDrawable.getIntrinsicWidth(),
                mClearDrawable.getIntrinsicHeight());
        // 默认设置隐藏图标
        setClearIconVisible(false);
        // 设置焦点改变的监听
        setOnFocusChangeListener(this);
        // 设置输入框里面内容发生改变的监听
        addTextChangedListener(this);
    }
    /**
     * 因为我们不能直接给EditText设置点击事件,所以我们用记住我们按下的位置来模拟点击事件 当我们按下的位置 在 EditText的宽度 -
     * 图标到控件右边的间距 - 图标的宽度 和 EditText的宽度 - 图标到控件右边的间距之间我们就算点击了图标,竖直方向就没有考虑
     */
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (event.getAction() == MotionEvent.ACTION_UP) {
            if (getCompoundDrawables()[2] != null) {
                boolean touchable = event.getX() > (getWidth() - getTotalPaddingRight())
                        && (event.getX() < ((getWidth() - getPaddingRight())));
                if (touchable && isClearable) {
                    this.setText("");
                }
            }
        }
        return super.onTouchEvent(event);
    }
    /**
     * 当ClearEditText焦点发生变化的时候,判断里面字符串长度设置清除图标的显示与隐藏
     */
    @Override
    public void onFocusChange(View v, boolean hasFocus) {
        this.hasFoucs = hasFocus;
        if (hasFocus) {
            setClearIconVisible(getText().toString().length() > 0);
        } else {
            setClearIconVisible(false);
        }
    }
    /**
     * 设置清除图标的显示与隐藏,调用setCompoundDrawables为EditText绘制上去
     *
     * @param visible
     */
    public void setClearIconVisible(boolean visible) {
        Drawable right = visible ? mClearDrawable : null;
        setCompoundDrawables(getCompoundDrawables()[0],
                getCompoundDrawables()[1], right, getCompoundDrawables()[3]);
    }
    /**
     * 当输入框里面内容发生变化的时候回调的方法
     */
    @Override
    public void onTextChanged(CharSequence s, int start, int count, int after) {
        if (hasFoucs) {
            setClearIconVisible(s.toString().length() > 0);
        }
    }
    @Override
    public void beforeTextChanged(CharSequence s, int start, int count,
                                  int after) {
    }
    @Override
    public void afterTextChanged(Editable s) {
    }
    /**
     * 设置晃动动画
     */
    public void startShakeAnimation() {
        this.startAnimation(shakeAnimation(5));
    }
    /**
     * 晃动动画
     *
     * @param counts 1秒钟晃动多少下
     * @return
     */
    public static Animation shakeAnimation(int counts) {
        Animation translateAnimation = new TranslateAnimation(0, 10, 0, 0);
        translateAnimation.setInterpolator(new CycleInterpolator(counts));
        translateAnimation.setDuration(1000);
        return translateAnimation;
    }
    /**
     * 设置是否可清除
     *
     * @param clearable
     */
    public void setClearable(boolean clearable) {
        isClearable = clearable;
    }
    /**
     * 获取是否可清除
     *
     * @return
     */
    public boolean getClearable() {
        return isClearable;
    }
}
app/src/main/res/layout/activity_login.xml
New file
@@ -0,0 +1,161 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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:id="@+id/container"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    tools:context=".ui.login.view.LoginActivity">
    <com.duqing.missions.widgets.TitleBarView
        android:id="@+id/titleBar"
        android:layout_width="match_parent"
        android:layout_height="50dp"
        android:layout_marginBottom="150dp"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:leftDrawable="@mipmap/icon_back_black"/>
    <TextView
        android:id="@+id/txt_password_title"
        android:layout_width="wrap_content"
        android:layout_height="40dp"
        android:layout_marginTop="20dp"
        android:gravity="center"
        android:text="密码登录"
        android:textSize="17sp"
        android:textStyle="bold"
        app:layout_constraintTop_toBottomOf="@+id/titleBar"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toLeftOf="@id/txt_verify_title"/>
    <TextView
        android:id="@+id/txt_verify_title"
        android:layout_width="wrap_content"
        android:layout_height="40dp"
        android:layout_marginTop="20dp"
        android:text="短信登录"
        android:gravity="center"
        app:layout_constraintTop_toBottomOf="@+id/titleBar"
        app:layout_constraintLeft_toRightOf="@id/txt_password_title"
        app:layout_constraintRight_toRightOf="parent"/>
    <com.duqing.missions.widgets.ClearEditText
        android:id="@+id/edit_phone"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="请输入手机号"
        android:inputType="phone"
        android:maxLength="11"
        android:selectAllOnFocus="true"
        android:layout_marginTop="26dp"
        app:layout_constraintTop_toBottomOf="@id/txt_password_title"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent" />
    <androidx.constraintlayout.widget.ConstraintLayout
        android:id="@+id/container_password"
        android:layout_width="match_parent"
        android:layout_height="180dp"
        app:layout_constraintTop_toBottomOf="@id/edit_phone"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent">
        <com.duqing.missions.widgets.ClearEditText
            android:id="@+id/edit_password"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_marginTop="8dp"
            android:hint="请输入密码"
            android:imeActionLabel="@string/action_sign_in_short"
            android:imeOptions="actionDone"
            android:inputType="textPassword"
            android:selectAllOnFocus="true"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"/>
        <TextView
            android:id="@+id/text_register"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            app:layout_constraintHorizontal_chainStyle="spread_inside"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintTop_toBottomOf="@id/edit_password"
            app:layout_constraintRight_toLeftOf="@id/text_forgot"
            android:paddingTop="@dimen/frame_margin_lr"
            android:paddingLeft="3dp"
            android:text="注册账号" />
        <TextView
            android:id="@+id/text_forgot"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="忘记密码"
            android:paddingTop="@dimen/frame_margin_lr"
            android:paddingRight="3dp"
            app:layout_constraintHorizontal_chainStyle="spread_inside"
            app:layout_constraintTop_toBottomOf="@id/edit_password"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintLeft_toRightOf="@id/text_register" />
    </androidx.constraintlayout.widget.ConstraintLayout>
    <androidx.constraintlayout.widget.ConstraintLayout
        android:id="@+id/container_verify"
        android:layout_width="match_parent"
        android:layout_height="180dp"
        android:visibility="gone"
        app:layout_constraintTop_toBottomOf="@id/edit_phone"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent">
        <EditText
            android:id="@+id/edit_verify"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_marginTop="8dp"
            android:hint="请输入验证码"
            android:imeActionLabel="@string/action_sign_in_short"
            android:imeOptions="actionDone"
            android:inputType="number"
            android:selectAllOnFocus="true"
            android:maxLength="4"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"/>
        <TextView
            android:id="@+id/text_verify"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="获取验证码"
            android:textSize="16sp"
            android:textColor="@color/deep_sky"
            android:paddingRight="5dp"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="@id/edit_verify"
            app:layout_constraintBottom_toBottomOf="@id/edit_verify" />
    </androidx.constraintlayout.widget.ConstraintLayout>
    <Button
        android:id="@+id/login"
        android:layout_width="200dp"
        android:layout_height="wrap_content"
        android:layout_gravity="start"
        android:layout_marginTop="16dp"
        android:layout_marginBottom="64dp"
        android:enabled="false"
        android:text="登录"
        android:textColor="@color/white"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.501"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/container_password"
        app:layout_constraintVertical_bias="0.0"
        app:layout_goneMarginTop="180dp" />
</androidx.constraintlayout.widget.ConstraintLayout>
app/src/main/res/mipmap-hdpi/icon_back_black.png
app/src/main/res/mipmap-hdpi/icon_white_back.png
app/src/main/res/mipmap-xhdpi/icon_back_black.png
app/src/main/res/mipmap-xhdpi/icon_white_back.png
app/src/main/res/mipmap-xxhdpi/icon_back_black.png
app/src/main/res/mipmap-xxhdpi/icon_delete.png
app/src/main/res/mipmap-xxhdpi/icon_white_back.png
app/src/main/res/mipmap-xxxhdpi/icon_back_black.png
app/src/main/res/mipmap-xxxhdpi/icon_white_back.png
app/src/main/res/values-night/themes.xml
@@ -1,13 +1,13 @@
<resources xmlns:tools="http://schemas.android.com/tools">
    <!-- Base application theme. -->
    <style name="Theme.Missions" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
    <style name="Theme.Missions" parent="Theme.MaterialComponents.DayNight.NoActionBar">
        <!-- Primary brand color. -->
        <item name="colorPrimary">@color/purple_200</item>
        <item name="colorPrimaryVariant">@color/purple_700</item>
        <item name="colorPrimary">@color/sky</item>
        <item name="colorPrimaryVariant">@color/red</item>
        <item name="colorOnPrimary">@color/black</item>
        <!-- Secondary brand color. -->
        <item name="colorSecondary">@color/teal_200</item>
        <item name="colorSecondaryVariant">@color/teal_200</item>
        <item name="colorSecondary">@color/black_4</item>
        <item name="colorSecondaryVariant">@color/black_4</item>
        <item name="colorOnSecondary">@color/black</item>
        <!-- Status bar color. -->
        <item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
app/src/main/res/values/colors.xml
@@ -1,9 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <color name="purple_200">#FFBB86FC</color>
    <color name="purple_500">#FF6200EE</color>
    <color name="purple_700">#FF3700B3</color>
    <color name="teal_200">#FF03DAC5</color>
    <color name="teal_700">#FF018786</color>
    <color name="black">#FF000000</color>
    <color name="black_4">#373737</color>
@@ -12,6 +8,7 @@
    <color name="red">#FF1414</color>
    <color name="gray">#CDCDCD</color>
    <color name="enable">#ECECEC</color>
    <color name="deep_sky">#4184D6</color>
    <color name="sky">#509CFA</color>
    <color name="gold">#FAD550</color>
</resources>
app/src/main/res/values/strings.xml
@@ -3,4 +3,12 @@
    <string name="title_home">Home</string>
    <string name="title_dashboard">Dashboard</string>
    <string name="title_notifications">Notifications</string>
    <string name="prompt_email">Email</string>
    <string name="prompt_password">Password</string>
    <string name="action_sign_in">Sign in or register</string>
    <string name="action_sign_in_short">Sign in</string>
    <string name="welcome">"Welcome !"</string>
    <string name="invalid_username">Not a valid username</string>
    <string name="invalid_password">Password must be >5 characters</string>
    <string name="login_failed">"Login failed"</string>
</resources>
app/src/main/res/values/styles.xml
@@ -27,4 +27,16 @@
        <item name="android:textColor">@color/black_4</item>
        <item name="android:layout_weight">1</item>
    </style>
    <declare-styleable name="TitleBarView">
        <attr name="leftDrawable" format="reference" />
        <attr name="rightDrawable" format="reference" />
        <attr name="rightDrawablePadding" format="dimension" />
        <attr name="titleText" format="string" />
        <attr name="titleTextSize" format="dimension" />
        <attr name="titleTextColor" format="color" />
        <attr name="rightText" format="string" />
        <attr name="rightTextSize" format="dimension" />
        <attr name="rightTextColor" format="color" />
    </declare-styleable>
</resources>
app/src/main/res/values/themes.xml
@@ -1,13 +1,13 @@
<resources xmlns:tools="http://schemas.android.com/tools">
    <!-- Base application theme. -->
    <style name="Theme.Missions" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
    <style name="Theme.Missions" parent="Theme.MaterialComponents.Light.NoActionBar">
        <!-- Primary brand color. -->
        <item name="colorPrimary">@color/purple_500</item>
        <item name="colorPrimaryVariant">@color/purple_700</item>
        <item name="colorOnPrimary">@color/white</item>
        <item name="colorPrimary">@color/sky</item>
        <item name="colorPrimaryVariant">@color/red</item>
        <item name="colorOnPrimary">@color/black</item>
        <!-- Secondary brand color. -->
        <item name="colorSecondary">@color/teal_200</item>
        <item name="colorSecondaryVariant">@color/teal_700</item>
        <item name="colorSecondary">@color/black_4</item>
        <item name="colorSecondaryVariant">@color/black_4</item>
        <item name="colorOnSecondary">@color/black</item>
        <!-- Status bar color. -->
        <item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>