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>