34歲!100天!學會Java編程(Day81-Day98)—Android與Web應用一站式開發(fā)

西城高鐵

快到春節(jié)了,三年多沒回過老家,今年打算回家看看,跟老媽在老家過個年,帶點小酒給老爸掃掃墓。一個月前西城高鐵開通,北京-成都坐火車只需要9個多小時,天知道這列火車是如何穿越大秦嶺的。打算體驗一次回家的高速列車,但是票一如既往的搶不著,只好買到初一的票,留在北京過除夕吧。

百日總結

這一篇主要是對之前學習Java的一點總結,Android應用與Web應用是企業(yè)應用常見的兩種形式。對于當前的創(chuàng)業(yè)性公司來說,多是把重心放在移動端App的開發(fā)上。但是個人覺得一開始設計時將兩者統(tǒng)一規(guī)劃,將核心的業(yè)務都放在服務器端,增加一些Web前端設計的工作,就可以實現(xiàn)一站式開發(fā),適應更廣的業(yè)務場景,這樣做還可以適當減輕Android端業(yè)務邏輯處理的壓力。

本案從Java零基礎開始,在工作之余用一百天的業(yè)余時間,對一個“娛樂社區(qū)”(代號CE)習作的不斷迭代升級開發(fā)過程中,完成了Android客戶端、Web前端、服務器后端完整框架的開發(fā),掌握了基本的全棧開發(fā)能力。

Web網(wǎng)站瀏覽和安卓apk下載地址:娛樂社區(qū)習作

(一)整體框架

整體框架

這個框架很容易理解,只是在之前Web應用的框架的基礎上,在服務器前端控制器增加了JSON數(shù)據(jù)的交互接口,用于與Android端進行遠程交互。

(二)統(tǒng)一設計風格

Android與Web應用一站式開發(fā)要求將兩者作為一個產(chǎn)品考慮,會要求統(tǒng)一的設計風格。

由于Web前端設計與Android前端設計的方法相差較大,通常不會是由一個設計師開展設計,甚至對于稍大的應用,光是其一就不只一個設計師進行設計。不同的設計師的設計語言與理念通常也會有所區(qū)別,而過多的設計理念對于產(chǎn)品呈現(xiàn)會是一場災難。此時,設計規(guī)范可以很好地解決這個問題。

適用于Web前端與Android前端的通用設計規(guī)范通常有:色彩規(guī)范、文字規(guī)范等。

對于Android應用來說還需考慮布局規(guī)范、控件規(guī)范、圖標規(guī)范等。對于適用于手機瀏覽器的動態(tài)響應Web前端,也應盡量遵守Android設計規(guī)范。

以下是某應用的部分設計規(guī)范(示例):


設計規(guī)范示例

(三)android客戶端的工作

(1)CE(v7.0)APP客戶端架構
CE(v7.0)APP客戶端架構

在設計過程中,考慮下面幾個數(shù)據(jù)層盡量使用外觀模式,簡化接口。

(2)開發(fā)文檔結構

開發(fā)工具使用AS,此部分大體上是上述APP架構的具體實現(xiàn),由于時間有限,對功能進行了部分閹割,所以實際的開發(fā)項目會比這個復雜。




文檔結構

其中,資源res部分還有不少東西,限于篇幅就不展開了。在實現(xiàn)過程中發(fā)現(xiàn)各層和模塊之間交叉太多,想要實現(xiàn)外觀模式并不容易,下次再想想辦法。

(3)AndroidManifest.xml

這部分重點是應用權限的獲取,應用名稱和圖標的修改,四大天王的注冊等基本設置。我曾經(jīng)因為沒有設置權限,訪問不了網(wǎng)絡;曾經(jīng)編寫一個新的Activity后沒有注冊,導致跳轉失敗還花了些時間找原因。

(4)build.gradle(Module:app)

這個文件很重要,其中包含了SDK版本的配置,app應用版本的管理,MVVP數(shù)據(jù)綁定設置,以及最重要的中央倉庫管理。僅列舉我這個應用所用到的框架和庫:

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    //Support
    implementation 'com.android.support:appcompat-v7:26.1.0'
    implementation 'com.android.support.constraint:constraint-layout:1.0.2'
    //Material Design
    implementation 'com.android.support:design:26.1.0'
    //Vectro-Drawable
    implementation 'com.android.support:support-vector-drawable:26.1.0'
    //RecyclerView
    implementation 'com.android.support:recyclerview-v7:26.1.0'
    //CardView
    implementation 'com.android.support:cardview-v7:26.1.0'
    //ButterKnife
    implementation 'com.jakewharton:butterknife:8.8.1'
    annotationProcessor 'com.jakewharton:butterknife-compiler:8.8.1'
    //BottomTabBar
    implementation 'com.hjm:BottomTabBar:1.1.1'
    //Retrofit,OkHttp,RxJava
    implementation 'com.squareup.retrofit2:retrofit:2.3.0'
    implementation 'com.squareup.retrofit2:converter-gson:2.3.0'
    implementation 'com.squareup.retrofit2:adapter-rxjava:2.3.0'
    implementation 'com.google.code.gson:gson:2.6.1'
    implementation 'com.squareup.okhttp3:okhttp:3.9.1'
    implementation 'com.squareup.okhttp3:logging-interceptor:3.9.1'
    implementation 'com.squareup.okhttp3:okhttp-urlconnection:3.9.1'
    implementation 'com.squareup.okio:okio:1.13.0'
    implementation 'com.jakewharton.rxbinding:rxbinding:0.4.0'
    implementation 'io.reactivex:rxandroid:1.2.1'
    implementation 'io.reactivex:rxjava:1.1.6'
    //Test
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'com.android.support.test:runner:1.0.1'
    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1'
    //media, animation
    implementation 'com.github.bumptech.glide:glide:4.0.0'
    implementation 'com.nineoldandroids:library:2.4.0'
    //servlet
    implementation 'javax.servlet:javax.servlet-api:4.0.0'
}
(5)主要功能開發(fā)要點

1.底部Tab主菜單的實現(xiàn)

實現(xiàn)底部Tab主菜單,有很多選擇,我比較了一下material design庫中的BottomNavigationView和第三方支持庫BottomTabBar。

BottomTabBar的突出優(yōu)點就是實現(xiàn)簡單,只需要短短幾行代碼就可以搞定:

mBottomTabBar.init(getSupportFragmentManager())
                .addTabItem("首頁", R.drawable.icon1, HomeFragment.class)
                .addTabItem("發(fā)現(xiàn)", R.drawable.icon2, DiscoverFragment.class)
                .addTabItem("發(fā)起", R.drawable.icon3, PublishFragment.class)
                .addTabItem("圈子", R.drawable.icon4, CircleFragment.class)
                .addTabItem("我的", R.drawable.icon5, MineFragment.class);

而BottomNavigationView實現(xiàn)起來代碼則要多很多:

   private void initFragments() {
        fragment1 = new HomeFragment();
        fragment2 = new DiscoverFragment();
        fragment3 = new PublishFragment();
        fragment4 = new CircleFragment();
        fragment5 = new MineFragment();
    }

    public boolean switchFragment(int fragmentid){
        transaction=getSupportFragmentManager().beginTransaction();
        lastShowFragment=fragmentid;
        boolean flag;
        switch (fragmentid) {
            case R.id.navigation_home:
                transaction.replace(R.id.fragment_container,fragment1).commit();
                return true;
            case R.id.navigation_discover:
                transaction.replace(R.id.fragment_container,fragment2).commit();
                return true;
            case R.id.navigation_publish:
                flag=LoginInterceptor.ContinueOrLogin(this,R.id.navigation_publish);
                if(!flag)transaction.replace(R.id.fragment_container,fragment3).commit();
                return true;
            case R.id.navigation_circle:
                flag=LoginInterceptor.ContinueOrLogin(this,R.id.navigation_circle);
                if(!flag)transaction.replace(R.id.fragment_container,fragment4).commit();
                return true;
            case R.id.navigation_mine:
                flag=LoginInterceptor.ContinueOrLogin(this,R.id.navigation_mine);
                if(!flag)transaction.replace(R.id.fragment_container,fragment5).commit();
                return true;
            default:
                lastShowFragment=-1;
                return false;
        }
    }

    private BottomNavigationView.OnNavigationItemSelectedListener mOnNavigationItemSelectedListener=
            new BottomNavigationView.OnNavigationItemSelectedListener() {
            @Override
            public boolean onNavigationItemSelected(@NonNull MenuItem item) {
                return switchFragment(item.getItemId());
            }
        };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
         navigation = (BottomNavigationView) findViewById(R.id.navigation);
navigation.setOnNavigationItemSelectedListener(mOnNavigationItemSelectedListener);
        BottomNavigationViewHelper.disableShiftMode(navigation);
        initFragments();
        if(savedInstanceState==null){
            switchFragment(R.id.navigation_home);
        }else{
            switchFragment(savedInstanceState.getInt("index",R.id.navigation_home));
        }
    }

public class BottomNavigationViewHelper {
    @SuppressLint("RestrictedApi")
    public static void disableShiftMode(BottomNavigationView view) {
        BottomNavigationMenuView menuView = (BottomNavigationMenuView) view.getChildAt(0);
        try {
            Field shiftingMode = menuView.getClass().getDeclaredField("mShiftingMode");
            shiftingMode.setAccessible(true);
            shiftingMode.setBoolean(menuView, false);
            shiftingMode.setAccessible(false);
            for (int i = 0; i < menuView.getChildCount(); i++) {
                BottomNavigationItemView item = (BottomNavigationItemView) menuView.getChildAt(i);
                //noinspection RestrictedApi
                item.setShiftingMode(false);
                // set once again checked value, so view will be updated
                //noinspection RestrictedApi
                item.setChecked(item.getItemData().isChecked());
            }
        } catch (NoSuchFieldException e) {
            Log.e("BNVHelper", "Unable to get shift mode field", e);
        } catch (IllegalAccessException e) {
            Log.e("BNVHelper", "Unable to change value of shift mode", e);
        }
    }

但是,最終我仍然選擇了BottomNavigationView,因為兩個原因:一是BottomTabBar不能使用AS自帶的大量矢量圖標,這是一大浪費??;二是因為,我都花了那么多時間把BottomNavigationView搞明白了,舍不得呀!?。?/p>

2.首頁RecyclerView復合布局的實現(xiàn)

所有實用app的首頁基本都采用的是復合布局,主框架采用RecyclerView,然后將主框架分成若干層,每一層采用不同的布局或者嵌套其他的View組件,比如ViewPager。

復合布局麻煩的地方在于,不同層的Item-Layout不同,需要使用不同的ViewHolder。具體的實現(xiàn)代碼太多就不貼了,只貼一個示意圖。


復合布局 RecyclerView

在此基礎上可以進一步衍生嵌套其他的View組件。

3.矢量圖形資源的使用

先上圖


矢量圖

這是一大寶庫呀,幾乎大部分常用圖標都能在里面找到,牛人還可以嘗試自建矢量圖。具體步驟為:右鍵點擊Drawable-->New-->Vector Asset。

4.TabLayout與ViewPager的聯(lián)動

這個也是安卓應用中常用的一種方式,實現(xiàn)起來相對簡單:

    private void initFragments() {
        fragment1 = new LoginFragment();
        fragment2 = new RegisterFragment();
        fragments=new Fragment[]{fragment1,fragment2};
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_log_reg);
        mViewPager=(ViewPager)findViewById(R.id.viewpager_log_reg);
        mTabLayout= (TabLayout) findViewById(R.id.tab_log_reg);
        //初始化
        initFragments();
        //設置viewpager Adapter
        FragmentManager fragmentManager=getSupportFragmentManager();
        mViewPager.setAdapter(new FragmentStatePagerAdapter(fragmentManager) {
            @Override
            public Fragment getItem(int position) {
                return fragments[position];
            }
            @Override
            public int getCount() {
                return fragments.length;
            }
            //解決TabLayout與ViewPager聯(lián)動后無標題問題?。。?            @Override
            public CharSequence getPageTitle(int position) {
                CharSequence[] list_title=new String[]{"登    錄","注    冊"};
                return list_title[position];
            }
        });
        //重點來了,實現(xiàn)聯(lián)動就靠這句
        mTabLayout.setupWithViewPager(mViewPager);
    }

5.使用SharedPreferences管理Cookie

SharedPreferences是安卓系統(tǒng)提供的一種數(shù)據(jù)持久化機制,使用較為簡潔,常用來存儲Cookie中的用戶登錄信息、應用版本信息等。

public class CookieUtils {
    protected static final String TAG="CE7";
    private final static String PREF_COOKIE_STRINGS="CE7_cookie_strings";
    private final static String LOGIN_STATUS="is_login";
    public static class AddCookiesInterceptor implements Interceptor {
        private Context context;
        public AddCookiesInterceptor(Context context) {
            this.context = context;
        }
        @Override
        public Response intercept(Chain chain) throws IOException {
            Request.Builder builder = chain.request().newBuilder();
            //讀取SharedPreferences
            HashSet<String> preferences = (HashSet) PreferenceManager.getDefaultSharedPreferences(context.getApplicationContext()).getStringSet(PREF_COOKIE_STRINGS, new HashSet<String>());

            for (String cookie : preferences) {
                builder.addHeader("Cookie", cookie);
                Log.d(TAG, "Adding Header: " + cookie); // This is done so I know which headers are being added; this interceptor is used after the normal logging of OkHttp
            }
            return chain.proceed(builder.build());
        }
    }

    public static class ReceivedCookiesInterceptor implements Interceptor {
        private Context context;
        public ReceivedCookiesInterceptor(Context context) {
            this.context = context;
        }
        @Override
        public Response intercept(Chain chain) throws IOException {
            Response originalResponse = chain.proceed(chain.request());
            if (!originalResponse.headers("Set-Cookie").isEmpty()) {
                HashSet<String> cookies = new HashSet<>();
                for (String header : originalResponse.headers("Set-Cookie")) {
                    cookies.add(header);
                }
            //寫入SharedPreferences
            PreferenceManager.getDefaultSharedPreferences(context.getApplicationContext()).edit()
                        .putStringSet(PREF_COOKIE_STRINGS, cookies)
                        .apply();
            }
            return originalResponse;
        }
    }
    //從SharedPreferences獲取
    public static HashSet<String> getPreferences(Context context){
         HashSet<String> preferences =(HashSet) PreferenceManager.getDefaultSharedPreferences(context.getApplicationContext()).getStringSet(PREF_COOKIE_STRINGS, new HashSet<String>());
         return preferences;
    }
    //從SharedPreferences獲取List<String>
    public static List<String> getCookieStringsList(Context context){
        List<String> mCookieStringsList=new ArrayList<>();
        for(String str:getPreferences(context)){
            mCookieStringsList.add(str);
        }
        return mCookieStringsList;
    }

    //從SharedPreferences或者response獲取List<Cookie>
    public static List<Cookie> getCookieList(Context context, List<String> mCookieStringsList){
        List<Cookie> mCookieList=new ArrayList<>();
        for(int i=0;i<mCookieStringsList.size();i++){
            String[] content=SplitCookieString(mCookieStringsList.get(i));
            Cookie cookie=new Cookie(content[0],content[1]);
            mCookieList.add(cookie);
        }
        return mCookieList;
    }

    //從SharedPreferences獲取CookiesKVMap
    public static Map<String,String> getCookieKvMap(Context context){
        Map<String,String> mCookieKvMap=new HashMap<>();
        List<Cookie> mCookieList=getCookieList(context,getCookieStringsList(context));
        for(int i=0;i<mCookieList.size();i++) {
          mCookieKvMap.put(mCookieList.get(i).getName(),mCookieList.get(i).getValue());
        }
        return  mCookieKvMap;
    }

    public static String[] SplitCookieString(String cookie_string){
        String[] firstsplit=cookie_string.split(";",2);
        String[] secondsplit=firstsplit[0].split("=",2);
        return secondsplit;
    }
}

先開發(fā)出一個基于SharedPreferences的CookieUtils類,然后基于此類在業(yè)務層可以開發(fā)出Cookie的攔截器、登錄令牌管理、退出登錄等業(yè)務邏輯。

(6)開發(fā)過程中的那些坑

簡直太多了,這個過程中我倒是熟練掌握了AS的調(diào)試方法。這里僅列舉我印象比較深刻的幾個事。

1.使用MVVP綁定ViewModel和Layout中圖片資源問題

問題描述:MVVP綁定傳統(tǒng)數(shù)據(jù)不成問題,但是綁定ImageView:src屬性時無法顯示。
解決辦法:在ActiveViewModel中添加一個適配器搞定:

    @BindingAdapter("android:src")
    public static void setSrc(ImageView view, int resId) {
        view.setImageResource(resId);
    }

2.在TableLayout中使用EditText:inputType="textMultiLine"控件無法自動換行問題。

問題描述:我有一個最多輸入100個字的EditText,放到TableLayout中無法自動換行了。
解決辦法:在EditText的屬性中加一行搞定:

android:layout_weight="0"

3.TabLayout與ViewPager聯(lián)動時,會自動刪除Item-Tab問題。

問題描述:這兩者關聯(lián)起來后,會莫名其妙刪除TabLayout的Item-Tab標簽。
解決辦法:在TabLayout中刪掉Item-Tab,然后在ViewPager的Adapter中加一段搞定:

            @Override
            public CharSequence getPageTitle(int position) {
                CharSequence[] list_title=new String[]{"登    錄","注    冊"};
                return list_title[position];
            }

還有太多槽點,就不一一列舉了。

(四)服務器端的工作

詳見完整Web應用開發(fā)與升級
這里補充JSON數(shù)據(jù)交互接口的編寫。

@RestController
public class AndroidCon {
    @Autowired
    private BLLServer bllserver;
    
    @ResponseBody  
    @RequestMapping(value="androidlogin", produces = "text/json;charset=UTF-8")
    public String androidlogin(HttpServletRequest request,HttpServletResponse response) throws JsonGenerationException, JsonMappingException, IOException {
        //解析請求表單數(shù)據(jù)
                String username=request.getParameter("username");
        String password1=request.getParameter("password1");
        String password2=request.getParameter("password2");
                //獲取cookies
                Cookie[] cookies=request.getCookies();
        //請求業(yè)務層
        int result=-1;
        if(username!=null&&password!=null) {
            result=bllserver.Login(username, password);
        }
                //添加cookie
        String mTime=String.valueOf(new SimpleDateFormat("yy-MM-dd HH:mm").format(new Date()));
        String   str=   java.net.URLEncoder.encode(mTime,"UTF-8"); 
        if(result==2) {                        
            Cookie mCookie_time=new Cookie("last_visit_time",str);
            Cookie mCookie_islogin=new Cookie("is_login", "true");
            mCookie_islogin.setMaxAge(60*60*24*10);
            str   =   java.net.URLEncoder.encode(username,"UTF-8"); 
            Cookie mCookie_username=new Cookie("user_name", str);
            response.addCookie(mCookie_time);
            response.addCookie(mCookie_islogin);
            response.addCookie(mCookie_username);
        }
        //返回響應數(shù)據(jù)
        HttpBean.Result r=new HttpBean().InitiateResult();
        r.setResult(String.valueOf(result));
        ObjectMapper mapper = new ObjectMapper(); 
        String jsonString = mapper.writeValueAsString(r);
        System.out.println("訪問成功,result="+r.getResult());
        return jsonString;
    }
    
    @RequestMapping(value="androidregister", produces = "text/plain;charset=UTF-8")
    public @ResponseBody String androidregister(HttpServletRequest request,HttpServletResponse response) throws JsonGenerationException, JsonMappingException, IOException {
        //解析請求數(shù)據(jù)
        //請求業(yè)務層
        //添加cookie
        //返回響應數(shù)
    }
    
    @ResponseBody  
    @RequestMapping(value="androidpublish", produces = "text/plain;charset=UTF-8")
    public String androidpublish(HttpServletRequest request,HttpServletResponse response) throws JsonGenerationException, JsonMappingException, IOException {
        //解析請求數(shù)據(jù)
        //請求業(yè)務層
        //添加cookie
        //返回響應數(shù)據(jù)
    }
}

為了在網(wǎng)站提供android App下載,在控制層增加了下載功能模塊:

@Controller
public class DownloadService {
    @Autowired
    private BLLServer bllserver;
    private final static String FILENAME="app.apk";
    
    @RequestMapping("apkdownload")
    public ResponseEntity<byte[]> download(HttpServletRequest request) throws IOException {
        String filePath=request.getServletContext().getRealPath("/WEB-INF/file/download/");
        String fileName=FILENAME;
        File file = new File(filePath+fileName);
        byte[] body = null;
        InputStream is = new FileInputStream(file);
        body = new byte[is.available()];
        is.read(body);
        HttpHeaders headers = new HttpHeaders();
        headers.add("Content-Disposition", "attchement;filename=" + file.getName());
        HttpStatus statusCode = HttpStatus.OK;
        ResponseEntity<byte[]> entity = new ResponseEntity<byte[]>(body, headers, statusCode);
        is.close();
        return entity;
    }

為了在提供用戶頭像和活動宣傳照片的上傳,在控制層增加了上傳功能模塊:

@Controller
public class UploadService {
    @RequestMapping("upload")
    public String upload(HttpServletRequest request,
           @RequestParam(value="file") MultipartFile originalfile) throws Exception {
       //如果原始文件不為空,寫入目標文件
       if(!originalfile.isEmpty()) {
           //上傳文件路徑        
           String path =request.getServletContext().getRealPath("/WEB-INF/file/upload/");
           //目標文件名
           String filename = originalfile.getOriginalFilename();
           //創(chuàng)建目標文件
           File targetfile = new File(path,filename);
           //判斷目標文件路徑是否存在,如果不存在就創(chuàng)建一個
           if (!targetfile.getParentFile().exists()) { 
               targetfile.getParentFile().mkdirs();
           }
           //將上傳文件保存到一個目標文件當中
           originalfile.transferTo(targetfile);
           return "index";
       } else {
           return "hello";
       }
    }
    
    public static String uploadimg(HttpServletRequest request,MultipartFile originalfile) throws Exception {
        String visit_path=null;
       //如果原始文件不為空,寫入目標文件
       if(!originalfile.isEmpty()) {
           //目標文件路徑
           String path=request.getServletContext().getRealPath("/WEB-INF/file/upload/");
           //目標文件名
           String filename = originalfile.getOriginalFilename();
           String[] departname=filename.split("\\.",2);
           UUID fileid=UUID.randomUUID();
           String targetfilename=fileid.toString()+"."+departname[1];
           //創(chuàng)建目標文件
           File targetfile = new File(path,targetfilename);
           //判斷目標文件路徑是否存在,如果不存在就創(chuàng)建一個
           if (!targetfile.getParentFile().exists()) { 
               targetfile.getParentFile().mkdirs();
           }
           //將上傳文件保存到一個目標文件當中
           originalfile.transferTo(targetfile);
           //創(chuàng)建訪問路徑
           visit_path="/CommunityEntertain6/file/upload/"+targetfilename;
           return visit_path;
       } else {
           return "error";
       }
    }
}

其中,第一個方法供客戶端直接調(diào)用,第二個方法供服務器內(nèi)部調(diào)用,為每一張上傳的圖片生成唯一的文件名存儲在文件夾中,并且將文件路徑保存在用戶和活動數(shù)據(jù)庫中。

(五)web前端的工作

詳見Web前端編程
本部分補充了用戶頭像和活動照片上傳功能,并且在圖片上傳前提供圖片預覽功能。圖片預覽的Html和JavaScript代碼如下:

    <form method="post" action="postregister" enctype="multipart/form-data"  class="form-group">
      <h3>請您注冊</h3>
      <br>
      <p class="form-inline">
        <label class="input-group">用戶名&emsp;&emsp;</label>
        <input type="text" name="username" class="form-control" placeholder="username">
      </p>
      <p class="form-inline">
        <label class="input-group">密&emsp;碼&emsp;&emsp;</label>
        <input type="password" name="password1" class="form-control" placeholder="password">
      </p>
      <p class="form-inline">
        <label class="input-group">密&emsp;碼&emsp;&emsp;</label>
        <input type="password" name="password2" class="form-control" placeholder="confirm password">
      </p>
      <p class="form-inline" >
      <table width="100%" border="0" cellspacing="0" cellpadding="0">  
        <tbody>  
            <tr>  
                <td align="center" style="padding-top:10px;">
                    <label class="form-control btn-primary" for="xFile"  style="display: block; width: 100px;">上傳頭像</label>
                </td>  
                <td height="101" align="center">  
                    <div id="localImag">
                        <img id="preview" src="" alt="portrait"  style="display: block; width: 180px; height: 150px;">
                    </div>  
                </td>  
            </tr>   
        </tbody>  
    </table>
        <div id="InfoDiv"></div>        
        <input type="file"  id="xFile" name="originalfile" accept="image/*" onchange="PreviewImg(this)" style="position:absolute;clip:rect(0 0 0 0);">
    </p>
      <p>
        <input type="submit" name="submit" class="form-control btn-primary" value="注&emsp;冊">
      </p>
</form>
<script type="text/javascript">  
//判斷瀏覽器是否支持FileReader接口
if (typeof FileReader == 'undefined') {
    document.getElementById("InfoDiv").InnerHTML = "<h1>當前瀏覽器不支持FileReader接口</h1>";
    //使選擇控件不可操作
    document.getElementById("xFile").setAttribute("disabled", "disabled");
}
//選擇圖片,馬上預覽
function PreviewImg(obj) {
    var file = obj.files[0];
    var reader = new FileReader();
    reader.onload = function (e) {
        var img = document.getElementById("preview");
        img.src = e.target.result;
        //或者 img.src = this.result;  //e.target == this
    }
    reader.readAsDataURL(file);
}
</script>

(六)畢業(yè)感言

學習Java是我設定的第一個百日計劃,目的是通過上班之外的業(yè)余時間自學,基本掌握Android和Web應用全棧編程的能力。目前看來基本能力已經(jīng)具備,尚缺的是更加底層的知識、不同行業(yè)應用場景的應對、以及用戶界面的設計。

這些都還需要花大量時間,不過基于對自己的定位,下一階段我的目標是向行業(yè)專業(yè)領域進軍,將自己對于生活的想象力釋放出來。

眼下最緊要的,是……休息……休息,好好……補覺。

上兩張圖紀念我的第一個百日計劃。


我的服務器

我的戰(zhàn)果
最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容