SimpleWord 簡詞開發記錄筆記

2015-8-6 15:07:54

詞庫

找不到詞庫,暫時找到一個帶音標和釋義的考研單詞excel(估計是好幾年前的大綱詞彙),就先用這個吧。
excel不能顯示音標的話,還得下載字體TOPhonetic.ttf。

數據庫

excel導入SQLite
試了幾個可視化工具,就SQLiyeStudio比較滿意,也沒有亂碼。
開始時把excel另存爲.csv文件,系統的分隔符是逗號,但是去控制面板改了,excel導出時還是逗號,不知道爲什麼,可能需要重啓電腦吧,懶得重啓。竟然忘記replace了!所以excel → 製表符分隔的txt → replace分隔符,就ok了^_^

優化——延遲切換fragment

public void switchContent(Fragment fragment) {
    contentFragment = fragment;
    getFragmentManager().beginTransaction().replace(R.id.content_frame, fragment).commit();
//  sm.showContent();
    Handler h = new Handler();
    h.postDelayed(new Runnable() {
        public void run() {
            sm.showContent();
        }
    }, 50);
}

2015-8-10

單詞本界面想做成卡片來滑動

RecyclerView

關於引入V7包:http://wp.aesean.com/?p=185

2015-8-11

  1. 繼承RecyclerView.Adapter出現The hierarchy of the type ViewHolder is inconsistent,因爲菜單用的SlidingMenu,support-v4包可能不是最新的,將SlidingMenu的libs下的v4包替換成最新的就可以了。
  2. Call requires API level 21 (current min is 17): android.content.Context#getDrawable
    解決

    ContextCompat.getDrawable(this, R.drawable.your_drawable)

2015-8-12

在actionbar添加spinner下拉列表

1.actionbar.xml

<?xml version="1.0" encoding="utf-8"?>  
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"  
android:layout_width="match_parent"  
android:layout_height="match_parent"  
android:gravity="center_vertical"  
android:orientation="horizontal" >  

    <TextView  
        android:id="@+id/action_bar_title"  
        android:layout_width="wrap_content"  
        android:layout_height="wrap_content"          
        android:text="下拉列表" />  

    <!-- 下拉列表  -->  
    <Spinner  
        android:id="@+id/action_bar_spinner"  
        android:layout_width="wrap_content"  
        android:layout_height="wrap_content"  
        >  
    </Spinner>  

</LinearLayout>  

2.在activity的onCreate()或fragment的onCreateView()中添加代碼,初始化下拉列表數據

//使自定義的普通View能在title欄顯示,actionBar.setCustomView能起作用
getActivity().getActionBar().setDisplayShowCustomEnabled(true); 
//初始化下拉列表
View actionbarLayout = view.inflate(activity,R.layout.actionbar, null);  
mActionbarSpinner = (Spinner) actionbarLayout.findViewById(R.id.actionbar_spinner);  

//初始化下拉列表數據
//方法一  
initSpinnerMethod1();  
//方法二  
//initSpinnerMethod2();  

//事件監聽  
mActionbarSpinner.setOnItemSelectedListener(new SpinnerItemSelectedListener());  

//在Bar上顯示定製view  
actionBar.setCustomView(actionbarLayout); 

初始化下拉列表數據:
(1) 在strings.xml添加數組

<string-array name="spinner_list" >  
<item>item1</item>  
<item>item2</item>  
<item>item3</item>   
</string-array>  

代碼:

private void initSpinnerMethod1() {  
     String[] mItems = getResources().getStringArray(R.array.spinner_list);  
     ArrayAdapter<String> spinnerAdapter = new ArrayAdapter<String>(activity,android.R.layout.simple_spinner_item, mItems);  
     spinnerAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);  
     mActionbarSpinner.setAdapter(spinnerAdapter);  
}  

(2)在代碼中添加數組

private List<String> getData(){          
    List<String> data = new ArrayList<String>();  
    data.add("item1");  
    data.add("item2");  
    data.add("item3");          
    return data;  
}  

private void initSpinnerMethod2()  {  
    mActionbarSpinner.setAdapter(  
            new ArrayAdapter<String>(activity,   
                    android.R.layout.simple_expandable_list_item_1,getData()));    
}  

3.監聽action_bar的spinner的item選擇事件

private class SpinnerItemSelectedListener implements OnItemSelectedListener {  

    @Override  
    public void onItemSelected(AdapterView<?> arg0, View view, int position,long arg3) {  
         String str= arg0.getItemAtPosition(position).toString();  
         Toast.makeText(MainActivity.this, "你選擇的是:"+str, 2000).show();             
    }  

    @Override  
    public void onNothingSelected(AdapterView<?> arg0) {}  
}  

同一個activity的不同fragment顯示不同的actionbar

在單詞本界面的actionbar想顯示一個spinner下拉選擇單詞本(暫時未加入切換單詞本的功能),在WordBookFragment的onCreateView()裏設置自定義actionbar

getActivity().getActionBar().setDisplayShowCustomEnabled(true);

別的fragment裏設置

getActivity().getActionBar().setDisplayShowCustomEnabled(false);

或者加載另外的view。

不同fragment的標題

分別在onCreateView()裏設置

getActivity().setTitle("標題");

若寫成

getActivity().getActionBar().setTitle("標題");    

會將所有fragment設置成同一標題

在fragment裏獲取actionbar

cannot convert from android.support.v7.app.ActionBar to android.app.ActionBar

ActionBar actionBar = getActivity.getActionBar();

使用quick fix:

android.app.ActionBar actionBar;  

2015-8-18

控件左右對齊

使用RelativeLayout

android:layout_alignParentRight="true"

2015-8-19

判斷程序是否首次運行

使用SharedPreferences,在onCreate()裏:

SharedPreferences preferences;  
Editor editor;  
if (preferences.getBoolean("firststart", true)) {  //獲取boolean值,可爲缺省值,若缺省,則返回參數二的值
    editor = preferences.edit();  
    editor.putBoolean("firststart", false);  //若是首次,則改爲false
    editor.commit();  //提交
}  

2015-8-20

狀態欄通知

可刪除

NotificationCompat.Builder mBuilder =
    new NotificationCompat.Builder(this)
    .setSmallIcon(R.drawable.notification_icon)
    .setContentTitle("My notification")
    .setContentText("Hello World!");
// Creates an explicit intent for an Activity in your app
Intent resultIntent = new Intent(this, ResultActivity.class);
// The stack builder object will contain an artificial back stack for the
// started Activity.
// This ensures that navigating backward from the Activity leads out of
// your application to the Home screen.
TaskStackBuilder stackBuilder = TaskStackBuilder.create(this);
// Adds the back stack for the Intent (but not the Intent itself)
stackBuilder.addParentStack(ResultActivity.class);
// Adds the Intent that starts the Activity to the top of the stack
stackBuilder.addNextIntent(resultIntent);
PendingIntent resultPendingIntent =
        stackBuilder.getPendingIntent(
            0,
            PendingIntent.FLAG_UPDATE_CURRENT
        );
mBuilder.setContentIntent(resultPendingIntent);
NotificationManager mNotificationManager =
    (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
// mId allows you to update the notification later on.
mNotificationManager.notify(mId, mBuilder.build());

常駐狀態欄(顯示在“進行中/ongoing”)

Notification

  • 低於API 11

    notification.flags |= Notification.FLAG_NO_CLEAR;
    
  • 高於API 11 或者 使用Android Support Library

    //獲取狀態通知欄管理
    NotificationManager mNotificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); 
    //實例化通知欄構造器
    NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(this);  
    mBuilder.setSmallIcon(R.drawable.ic_launcher);//設置通知小圖標
        .setContentTitle("標題")  //設置通知欄標題
        .setContentText("內容")   //設置通知欄顯示內容
        .setContentIntent(getDefalutIntent(Notification.FLAG_AUTO_CANCEL)) //設置通知欄點擊意圖
        .setNumber(number) //設置通知集合的數量
        .setTicker("通知來啦") //通知首次出現在通知欄,帶上升動畫效果的
        .setWhen(System.currentTimeMillis())//通知產生的時間,會在通知信息裏顯示,一般是系統獲取到的時間
        .setPriority(Notification.PRIORITY_DEFAULT) //設置該通知優先級
        .setAutoCancel(true)//設置這個標誌當用戶單擊面板就可以讓通知將自動取消  
        .setOngoing(false)//ture,設置他爲一個正在進行的通知。他們通常是用來表示一個後臺任務,用戶積極參與(如播放音樂)或以某種方式正在等待,因此佔用設備(如一個文件下載,同步操作,主動網絡連接)
        .setDefaults(Notification.DEFAULT_VIBRATE)//向通知添加聲音、閃燈和振動效果的最簡單、最一致的方式是使用當前的用戶默認設置,使用defaults屬性,可以組合
    //Notification.DEFAULT_ALL  Notification.DEFAULT_SOUND 添加聲音 // requires VIBRATE permission
    

service前臺服務

在service裏

startForeground(mID, notification);     //設置前臺服務

關於服務service

  • 啓動service的時候,onCreate方法只有第一次會調用,onStartCommandonStart(已被淘汰)每次都被調用。onStartCommand會告訴系統如何重啓服務,如判斷是否異常終止後重新啓動,在何種情況下異常終止。
  • 2.0 API level之後,實現onStart等同於重寫onStartCommand並返回START_STICKY 。
  • 2.0 API level之後,onStart()方法被onStartCommand()取代了。
  • 必須在AndroidManifest.xml中註冊

    <service android:name="完整包名.ServiceNotification" />
    

啓動服務

mContext.startService(intent);

關閉服務

mContext.stopService(intent);

保存、恢復spinner的選中項

保存

onItemSelected()

int userChoice = spinner.getSelectedItemPosition();
SharedPreferences sharedPref = getSharedPreferences("FileName",0);
SharedPreferences.Editor prefEditor = sharedPref.edit();
prefEditor.putInt("userChoiceSpinner",usersChoice);
prefEditor.commit();

恢復

SharedPreferences sharedPref = getSharedPreferences("FileName",MODE_PRIVATE);
int spinnerValue = sharedPref.getInt"userChoiceSpinner",-1);
if(spinnerValue != -1) 
    spinner.setSelection(spinnerValue);

關於Activity生命週期該做的事情

onCreate()

  • 使用onCreate()初始化你的Activity:創建UI、爲類的變量分配引用、綁定數據到控件、創建Service和線程。
  • 爲避免快速的創建和銷燬對象引發額外的垃圾回收,如果你的應用程序正常創建一套對象,建議它們在onCreate()中創建,因爲在Activity的生命週期中它只被調用一次。
  • onCreate()裏面儘量少做事情,避免程序啓動太久都看不到界面。

onResume()

當從Paused狀態恢復activity時,系統會調用onResume()方法。
系統每次調用onResume()時,activity都處於前臺,包括第一次創建的時候。所以,應該實現onResume()來初始化那些在onPause方法裏面釋放掉的組件,並執行那些activity每次進入Resumed state都需要的初始化動作 (例如開始動畫與初始化那些只有在獲取用戶焦點時才需要的組件)

onPause()

  • 不應使用onPause()來保存用戶改變的數據 (例如填入表格中的個人信息) 到永久存儲(File或者DB)上。僅僅當確認用戶期待那些改變能夠被自動保存的時候(例如正在撰寫郵件草稿),才把那些數據存到永久存儲 。
  • 避免在onPause()時執行CPU-intensive 的工作,例如寫數據到DB,因爲它會導致切換到下一個activity變得緩慢(應該把那些heavy-load的工作放到onStop()去做)。
  • 如果activity實際上是要被Stop,爲了切換的順暢應減少在OnPause()方法裏面的工作量。
  • 停止動畫或者是其他正在運行的操作,那些都會導致CPU的浪費.
  • 提交在用戶離開時期待保存的內容(例如郵件草稿).
  • 釋放系統資源,例如broadcast receivers, sensors (比如GPS), 或者是其他任何會影響到電量的資源。

onStop()

  • 當activity調用onStop()方法, activity不再可見,並且應該釋放那些不再需要的所有資源。一旦activity停止了,系統會在需要內存空間時摧毀它的實例。極端情況下,系統會直接殺死我們的app進程,並不執行activity的onDestroy()回調方法, 因此我們需要使用onStop()來釋放資源,從而避免內存泄漏。
    所以我們應該使用onStop()來執行那些耗時的釋放資源的操作,例如往數據庫寫信息。
    -無論什麼原因導致activity停止,系統總是會在onStop()之前調用onPause()方法。

onDestroy()

大多數app並不需要實現這個方法,因爲局部類的references會隨着activity的銷燬而銷燬,並且我們的activity應該在onPause()onStop()中執行清除activity資源的操作。然而,如果activity含有在onCreate調用時創建的後臺線程,或者是其他有可能導致內存泄漏的資源,則應該在OnDestroy()時進行資源清理,殺死後臺線程。

除非程序在onCreate()方法裏面就調用了finish()方法,系統通常是在執行了onPause()onStop()之後再調用onDestroy()。在某些情況下,例如我們的activity只是做了一個臨時的邏輯跳轉的功能,它只是用來決定跳轉到哪一個activity,這樣的話,需要在onCreate裏面調用finish方法,這樣系統會直接調用onDestory,跳過生命週期中的其他方法。

與Activity生命週期結合的應用場景

  • 與廣播(Broadcast)結合
    onResume註冊廣播(registerLinstener),在onPause註銷廣播(unregisterLinstener)。
    例如:做”搖一搖”功能(傳感器)、監聽網絡變化,就可以在onResume中註冊監聽,在onPause裏註銷掉,已節省資源提高效率。
  • 與服務(Service)結合
    onStartCommand綁定服務(bindService),在onStop中取消綁定(unbindService)。
    例如:需要通過Service定時更新UI上的數據,而Activity的可見週期在onStartonStop之間,那麼就可以再onStart時啓動服務,在onStop時停止服務。爲了節約系統資源,除了提高用戶體驗以外,開發人員應儘可能的優化程序。
  • 與Cursor結合
    使用managedQuery讓Activity幫你管理Cursor的生命週期,不用自己去close。
  • 釋放資源
    可以在onDestory中釋放一些資源。比如可以在onDestory時調用MediaPlayer的release。

AlarmManager定時啓動Service

private static AlarmManager am;
private static PendingIntent pendingIntent;
/**
 * 使用 AlarmManager 來 定時啓動服務
 */
public static void startPendingIntent(Context context) {
    am = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
    Intent intent = new Intent(context, MyService.class);//啓動示例Service
    pendingIntent = PendingIntent.getService(context, 0, intent, 0);
    long interval = DateUtils.MINUTE_IN_MILLIS * 30;// 30分鐘一次
    long firstWake = System.currentTimeMillis() + interval;
    am.setRepeating(AlarmManager.RTC, firstWake, interval, pendingIntent);
}
public static void stopPendingIntent() {
    if (pendingIntent != null) {
        if ( am == null) {
            am = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE);
        }
        am.cancel(pendingIntent);
        pendingIntent.cancel();
    }
};

參考:android 使用AlarmManager定時啓動service

Timer與AlarmManager的區別

通過調用Thread類的start()方法來啓動一個線程,這時此線程處於就緒(可運行)狀態,但此時並沒有運行,它需要CPU時間片。一旦得到CPU時間片,就會執行run()方法。run()的方法體稱爲線程體,它包含了要執行的這個線程的內容,run()方法運行結束,此線程也隨即終止。

Android 平臺上常用的定時器主要有兩個:

  • Java的Timer
  • Android的AlarmManager

Timer

Java的Timer類可以用來計劃需要循環執行的任務。

簡單的說,一個Timer內部封裝裝了“一個Thread”和“一個TimerTask隊列”,這個隊列按照一定的方式將任務排隊處理。封裝的ThreadTimer的構造方法調用時被啓動,這個Threadrun方法按照條件去循環這個TimerTask隊列,然後調用TimerTaskrun方法。

但是,如果CPU進入了休眠狀態,那麼這個thread將會因爲失去CPU時間片而阻塞,從而造成我們需要的定時任務失效。上述定時任務失效的場景分析:假設定時任務的條件是到了時間xx:yy才能執行,但由於cpu休眠造成線程阻塞的關係,當前系統時間超過了這個時間,即便CPU從終端中恢復了,那麼由於條件不滿足,定時任務在這一次自然就失效了。

解決方案:它需要用WakeLock讓CPU保持喚醒狀態。但這樣會大量消耗手機電量,大大減短手機待機時間。這種方式不能滿足我們的需求。

注:TimerTask實現Runnable接口,但它的run方法只是簡單的在Timer中封裝的Thread中被調用,並未將其放在其它線程中執行。也就是說timer單線程執行的,那麼問題來了,爲何要這麼費勁的封裝一個Runnable接口又不進行多線程調用?

AlarmManager

AlarmManager是Android 系統封裝的用於管理RTC的模塊,RTC(Real Time Clock)是一個獨立的硬件時鐘,可以在 CPU休眠時正常運行,在預設的時間到達時,通過中斷喚醒CPU。這意味着,如果我們用 AlarmManager來定時執行任務,CPU 可以正常的休眠,只有在需要運行任務時醒來一段很短的時間。

參考:Timer與AlarmManager的區別

Switch開關

Switch mSwitch = (Switch)view.findViewById(R.id.setting_switch_notification_word); 
    mSwitch.setOnCheckedChangeListener(new OnCheckedChangeListener(){

        public void onCheckedChanged(CompoundButton buttonView,boolean isChecked) {
            if (isChecked) {    
                // switch on,開啓通知欄單詞
                intentNotiService = new Intent(mContext, ServiceNotification.class);
                pendingIntentNotiService = PendingIntent.getService(mContext, 0, intentNotiService, 0);
                minute = 1;
                startPendingIntent(pendingIntentNotiService);
            } else {
                //switch off,關閉通知欄單詞
                stopPendingIntent(pendingIntentNotiService);
            }
        }
    });

2015-8-21

解決定時啓動通知服務時pop up無效

在Service的onStartCommand()裏更新每次Notification需要更新的內容,如單詞信息。無需改動的信息在onCreate()裏初始化。

  • 原代碼:

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        mBuilder
            .setContentText(WordsDB.wordClass.toString())   //測試用的單詞信息
            .setWhen(System.currentTimeMillis())    //更新的時間
            .setTicker(WordsDB.wordClass.toString());   //在通知欄動畫向上彈出
        startForeground(notifyID, mBuilder.build());    //display in "ongoing"
        Log.d("通知欄單詞", WordsDB.wordClass.toString());
        return super.onStartCommand(intent, flags, startId);
    }
    

    按鈕點擊時會更新notification,也會在通知欄彈出提示。
    但使用AlarmManager定時啓動該service時,會更新內容,但不會在通知欄彈出提示,只能自己打開通知欄才能查看到更新。

  • 改動:使用NotificationManager.notify()

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        mBuilder
            .setContentText(WordsDB.wordClass.toString())
            .setWhen(System.currentTimeMillis())
            .setTicker(WordsDB.wordClass.toString());   //popup in Status Bar
        mNotificationManager.notify(notifyID, notification);    //update data
        startForeground(notifyID, mBuilder.build());    //display in "ongoing"
        Log.d("通知欄單詞", WordsDB.wordClass.toString());
        return super.onStartCommand(intent, flags, startId);
    }
    

notify必須在startForeground前面,先更新通知的內容,再顯示在前臺。
notifyID需一致。

參考:How do I update the notification text for a foreground service in Android?

2015-8-22

輸入自定義更新時間間隔

用了3個EditText,分別輸入小時、分鐘、秒,默認30秒更新一次,即00:00:30
獲取焦點時自動清空EditText,失去焦點時若未輸入任何內容,則回到默認值

關於離開EditText時無法失焦(觸摸EditText外的位置)

在其parent view設置xml屬性:

android:clickable="true"
android:focusable="true"
android:focusableInTouchMode="true"

隱藏軟鍵盤keyboard

1.在parent view設置

android:clickable="true" 
android:focusableInTouchMode="true" 

2.hideKeyboard() method

public void hideKeyboard(View view) {
    InputMethodManager inputMethodManager =(InputMethodManager)getSystemService(Activity.INPUT_METHOD_SERVICE);
    inputMethodManager.hideSoftInputFromWindow(view.getWindowToken(), 0);
}

參考:how to hide soft keyboard on android after clicking outside EditText?

2015-8-28

改進:通過廣播Broadcast啓動服務service

Broadcast靜態registerReceiver與動態註冊的區別

1.動態註冊的廣播永遠要快於靜態註冊的廣播,不管靜態註冊的優先級設置的多高,不管動態註冊的優先級有多低

2.動態註冊廣播不是常駐型廣播,也就是說廣播跟隨activity的生命週期。
注意: 在activity結束前,移除廣播接收器。

靜態註冊是常駐型,也就是說當應用程序關閉後,如果有信息廣播來,程序也會被系統調用自動運行。

3.在同一個優先級下,誰先啓動的快,誰將先接收到廣播。

動態註冊代碼:

MyBroadcastReceiver  broadcast= new MyBroadcastReceiver();
IntentFilter filter = new IntentFilter("action_name");
registerReceiver(broadcast, filter);

靜態註冊代碼(在Manifest.xml中添加):

<receiver android:name="com.example.MyReceiver" >
    <intent-filter>
        <action android:name="action_name" /> //可自定義action_name
    </intent-filter>
 </receiver>

2015-8-29

Slidingmenu的bug

在設置如下時

sm.setTouchModeAbove(SlidingMenu.TOUCHMODE_FULLSCREEN);
sm.setTouchModeBehind(SlidingMenu.TOUCHMODE_FULLSCREEN);

1.菜單裏的listview無法點擊,解決:
在Slidingmenu_lib裏,

  • 修改CustomViewAbove.java,將onInterceptTouchEvent()onTouchEvent()case MotionEvent.ACTION_DOWN:break;改爲return mQuickReturn;
    去掉initCustomViewAbove()裏的setInternalPageChangeListener()
  • 修改CustomViewBehind.java,將onInterceptTouchEvent()onTouchEvent()return分別改爲return mViewAbove.onInterceptTouchEvent(e);return mViewAbove.onTouchEvent(e);

2.上述步驟後,若aboveview無法滑動,則在aboveview的root layout裏添加android:clickable="true"

參考:All touch events are consumed by CustomViewAbove #446

2015-8-31

懸浮窗單詞

參考教程:Android桌面懸浮窗效果實現,仿360手機衛士懸浮窗效果

基本步驟

1.在Manifest.xml聲明權限

<!-- 顯示頂層浮窗 -->
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />

注意:在MIUI上需要在設置中打開本應用的“顯示懸浮窗”開關,並且重啓應用,否則懸浮窗只能顯示在本應用界面內,不能顯示在手機桌面上。
2.獲取WindowManager

// 獲取應用的Context
Context mContext = context.getApplicationContext();
// 獲取WindowManager
WindowManager mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);

3.獲取layout

LayoutInflater.from(mContext).inflate(R.layout.float_window, this);  
View view = findViewById(R.id.window_layout);  

4.參數設置

smallWindowParams = new LayoutParams();  
//LayoutParams.TYPE_PHONE: 全屏區域顯示(不含狀態欄)
//LayoutParams.TYPE_SYSTEM_ALERT: 全屏區域顯示(包含狀態欄區域,被狀態欄覆蓋)
//LayoutParams.TYPE_SYSTEM_ERROR: 全屏區域顯示(包含狀態欄區域,覆蓋在狀態欄上)
smallWindowParams.type = LayoutParams.TYPE_SYSTEM_ERROR;  
//設置圖片格式:背景透明
smallWindowParams.format = PixelFormat.RGBA_8888;  
//FLAG_LAYOUT_IN_SCREEN:可在狀態欄上顯示
smallWindowParams.flags =  LayoutParams.FLAG_NOT_TOUCH_MODAL | LayoutParams.FLAG_NOT_FOCUSABLE | LayoutParams.FLAG_LAYOUT_IN_SCREEN;  
smallWindowParams.gravity = Gravity.LEFT | Gravity.TOP;  
smallWindowParams.width = ViewSmallFloatWindow.viewWidth;  
smallWindowParams.height = ViewSmallFloatWindow.viewHeight;  
smallWindowParams.x = 0;  
smallWindowParams.y = Util.getStatusBarHeight(context);  

讓懸浮窗顯示在狀態欄上層

LayoutParams.TYPE_SYSTEM_ERROR //在全屏區域顯示(可在狀態欄顯示,覆蓋狀態欄)
LayoutParams.FLAG_NOT_TOUCH_MODAL | LayoutParams.FLAG_NOT_FOCUSABLE | LayoutParams.FLAG_LAYOUT_IN_SCREEN;

注:

  • 這樣設置下拉通知欄也會顯示在懸浮窗下層。
  • FLAG_NOT_TOUCH_MODALFLAG_NOT_FOCUSABLE任選其一都可以,若只設置FLAG_LAYOUT_IN_SCREEN會導致懸浮窗的焦點變成全屏(懸浮窗外無法操作)。smallWindowParams.flags = LayoutParams.FLAG_NOT_TOUCH_MODAL | LayoutParams.FLAG_NOT_FOCUSABLE | LayoutParams.FLAG_LAYOUT_IN_SCREEN | LayoutParams.FLAG_LAYOUT_INSET_DECOR | LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH;也可以。

WindowManager

  • addView:添加一個懸浮窗,
  • updateViewLayout:更新懸浮窗的參數,
  • removeView:移除懸浮窗。

WindowManager.LayoutParams這個類用於提供懸浮窗所需的參數,其中有幾個經常會用到的變量:

  • type:確定懸浮窗的類型,一般設爲2002,表示在所有應用程序之上,但在狀態欄之下。

  • flags:確定懸浮窗的行爲,比如說不可聚焦,非模態對話框等等,屬性非常多,大家可以查看文檔。

  • gravity:確定懸浮窗的對齊方式,一般設爲左上角對齊,這樣當拖動懸浮窗的時候方便計算座標。

  • x:確定懸浮窗的位置,如果要橫向移動懸浮窗,就需要改變這個值。

  • y:確定懸浮窗的位置,如果要縱向移動懸浮窗,就需要改變這個值。

  • width:指定懸浮窗的寬度。

  • height:指定懸浮窗的高度。

獲取屏幕寬度和高度

Display中getHeight()和getWidth()被廢棄

Display dp=getWindowManager().getDefaultDisplay();
int Height=dp.getHeight();  ---->The method getHeight() from the type Display is deprecated
int Width=dp.getWidth();    ---->The method getWidth() from the type Display is deprecated

替代的方法:

//WindowManager wm = this.getWindowManager();
WindowManager wm = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE);

//DisplayMetrics dm = new DisplayMetrics();
//wm.getDefaultDisplay().getMetrics(dm);
DisplayMetrics dm = Resources.getSystem().getDisplayMetrics();

SCREEN_WIDTH = dm.widthPixels;
SCREEN_HEIGHT = dm.heightPixels;

解析heightPixels和widthPixels:

public int     heightPixels    The absolute height of the display in pixels.
public int     widthPixels     The absolute width of the display in pixels.

參考:
1.Display中getHeight()和getWidth() 官方廢棄
2.How to get Screen metrics outside an Activity?

點擊手機“返回鍵”,移除懸浮窗

在懸浮窗的view類裏添加

@Override
public boolean dispatchKeyEvent(KeyEvent event) { 
    switch (event.getKeyCode()) {
    case KeyEvent.KEYCODE_BACK:
        MyWindowManager.removeBigWindow(getContext());
    //  MyWindowManager.createSmallWindow(getContext());
        return true;
    default:
        return super.dispatchKeyEvent(event); 
    }
}

觸摸懸浮窗外部區域,移除懸浮窗

在懸浮窗的view類裏添加

@Override
public boolean dispatchTouchEvent(MotionEvent event) {
    int x = (int) event.getX();
    int y = (int) event.getY();
    Rect rect = new Rect();
    //getGlobalVisibleRect方法的作用是獲取視圖在屏幕座標中的可視區域;
    //getLocalVisibleRect的作用是獲取視圖本身可見的座標區域,座標以自己的左上角爲原點(0,0)
    this.getGlobalVisibleRect(rect);
    if ( ! rect.contains(x, y) ) {
        MyWindowManager.removeBigWindow(getContext());
    //  MyWindowManager.createSmallWindow(getContext());
    }
    return super.dispatchTouchEvent(event);
}

參考:

  1. Android懸浮窗實現 使用WindowManager
  2. getGlobalVisibleRect和getLocalVisibleRect

獲取狀態欄高度

public static int getStatusBarHeight(Context context) {  
    if (statusBarHeight == 0) {  
        try {  
            Class<?> c = Class.forName("com.android.internal.R$dimen");  
            Object o = c.newInstance();  
            Field field = c.getField("status_bar_height");  
            int x = (Integer) field.get(o);  
            statusBarHeight = context.getResources().getDimensionPixelSize(x);  
        } catch (Exception e) {  
            e.printStackTrace();  
        }  
    }  
    return statusBarHeight;  
}

參考:Android桌面懸浮窗效果實現,仿360手機衛士懸浮窗效果

TextView橫向自動滾動實現跑馬燈效果

1.原生android自帶的跑馬燈效果,直接配置TextView屬性

android:singleLine="true"  
android:ellipsize="marquee" 
android:focusable="true"  
android:focusableInTouchMode="true" 
android:marqueeRepeatLimit="marquee_forever"  
  • android:singleLine=true 表示使用單行文字,多行文字也就無所謂使用Marquee效果了。
  • android:marqueeRepeatLimit,設置走馬燈滾動的次數。marquee_forever爲無限循環。
  • android:ellipsize,設置了文字過長時如何切斷文字,可以有none, start,middle, end, 如果使用走馬燈效果則設爲marquee.
  • android:focusable,Android的缺省行爲是在控件獲得Focus時纔會顯示走馬燈效果

判斷當前界面是否是桌面

private boolean isHome() {  
    ActivityManager mActivityManager = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);  
    List<RunningTaskInfo> rti = mActivityManager.getRunningTasks(1);  
    return getHomes().contains(rti.get(0).topActivity.getPackageName());  
} 

獲得屬於桌面的應用的應用包名稱

private List<String> getHomes() {  
    List<String> names = new ArrayList<String>();  
    PackageManager packageManager = this.getPackageManager();  
    Intent intent = new Intent(Intent.ACTION_MAIN);  
    intent.addCategory(Intent.CATEGORY_HOME);  
    List<ResolveInfo> resolveInfo = packageManager.queryIntentActivities(intent,  
            PackageManager.MATCH_DEFAULT_ONLY);  
    for (ResolveInfo ri : resolveInfo) {  
        names.add(ri.activityInfo.packageName);  
    }  
    return names;  //返回包含所有包名的字符串列表
}  

2015-9-3

PendingIntent

flag

  • 0:默認爲無論是否存在相同的 PendingIntent 對象都會創建一個新的 PendingIntent。

  • FLAG_CANCEL_CURRENT: 如果當前系統中已經存在一個相同的 PendingIntent 對象,那麼就將先將已有的 PendingIntent 取消,然後重新生成一個 PendingIntent 對象。

  • FLAG_NO_CREATE: 如果當前系統中不存在相同的 PendingIntent 對象,系統將不會創建該 PendingIntent 對象而是直接返回 null。

  • FLAG_ONE_SHOT: 該 PendingIntent 只作用一次,如果該 PendingIntent 對象已經觸發過一次,那麼下次再獲取該 PendingIntent 並且再觸發時,系統將會返回一個 SendIntentException,在使用這個標誌的時候一定要注意哦。

  • FLAG_UPDATE_CURRENT: 如果系統中已存在該 PendingIntent 對象,那麼系統將保留該 PendingIntent 對象,但是會使用新的 Intent 來更新之前 PendingIntent 中的 Intent 對象數據,例如更新 Intent 中的 Extras。這個非常有用,例如之前提到的,我們需要在每次更新之後更新 Intent 中的 Extras 數據,達到在不同時機傳遞給 MainActivity 不同的參數,實現不同的效果。

參考:
1.What happens if you set the flag on a PendingIntent to 0?
2.Android PendingIntent 的一些小迷惑

2015-9-5

SeekBar拖動條

SeekBar總是自動獲取焦點(點擊/觸摸SeekBar外部區域時,thumb會有響應(變色))

.xml中給添加屬性:

android:clickable="true"
android:focusable="true"
android:focusableInTouchMode="true"

TextView顯示音標

1.下載音標字體,如TOPhonetic.ttf,將音標字體文件放在assets/font目錄下
2.爲TextView設置屬性

Typeface mFace = Typeface.createFromAsset(getAssets(), "font/TOPhonetic.ttf"); 
wordPhoneticTextView.setTypeface(mFace);

若顯示getAssets() is undefined,則改爲context.getAssets()
參考:Android如何顯示音標

2015-9-6

TextView設置不同的字體風格

懸浮窗裏只用了一個TextView,便於顯示跑馬燈效果。爲了顯示音標使用了TOPhonetic字體,但這樣顯示的英文和中文都不太喜歡,想只有音標使用TOPhonetic字體,單詞和釋義跟隨手機當前字體。

1.使用Html.fromHtml

如:

mTextView.setTextView(Html.fromHtml("<font color='red'><b>" + "紅色字體"
            + "</b></font>TextView學習顯示不同顏色"));

Textview並不支持所有的html標籤。如果更復雜的,可以直接使用webview組件。
查找資料有人說<font face="verdana" color="green">This is some text!</font>,想要更改字體必須手機上安裝了此字體。(未測試是否可行,但直接使用的確無效)

String Resources支持的tag

- <a> (supports attributes "href")
- <annotation>
- <b>
- <big>
- <font> (supports attributes "height", "size", "fgcolor" and "bicolor", as integers)
- <i>
- <li>
- <marquee>
- <small>
- <strike>
- <sub>
- <sup>
- <tt>
- <u>

Html.fromHtml()支持的tag

- <a> (supports attribute "href")
- <b>
- <big>
- <blockquote>
- <br>
- <cite>
- <dfn>
- <div>
- <em>
- <font> (supports attributes "color" and "face")
- <i>
- <img> (supports attribute "src". Note: you have to include an ImageGetter to handle retrieving a Drawable for this tag)
- <p>
- <small>
- <strong>
- <sub>
- <sup>
- <tt>
- <u>

支持的顏色:

- aqua
- black
- blue
- fuchsia
- green
- grey
- lime
- maroon
- navy
- olive
- purple
- red
- silver
- teal
- white
- yellow

參考:
1.HTML in TextViews
2.Android 字體設置注意的地方

2.使用Spannable

1.CustomTypefaceSpan Class:

import android.graphics.Paint;
import android.graphics.Typeface;
import android.text.TextPaint;
import android.text.style.TypefaceSpan;

public class CustomTypefaceSpan extends TypefaceSpan {
    private final Typeface newType;

    public CustomTypefaceSpan(String family, Typeface type) {
        super(family);
        newType = type;
    }

    @Override
    public void updateDrawState(TextPaint tp) {
        applyCustomTypeFace(tp, newType);
    }

    @Override
    public void updateMeasureState(TextPaint paint) {
        applyCustomTypeFace(paint, newType);
    }

    private static void applyCustomTypeFace(Paint paint, Typeface tf) {
        int oldStyle;
        Typeface old = paint.getTypeface();
        if (old == null) {
            oldStyle = 0;
        } else {
            oldStyle = old.getStyle();
        }

        int fake = oldStyle & ~tf.getStyle();
        if ((fake & Typeface.BOLD) != 0) {
            paint.setFakeBoldText(true);
        }

        if ((fake & Typeface.ITALIC) != 0) {
            paint.setTextSkewX(-0.25f);
        }

        paint.setTypeface(tf);
    }
}

2.使用方法

Typeface font1 = Typeface.createFromAsset(getAssets(), "font/font1.ttf");
Typeface font2 = Typeface.createFromAsset(getAssets(), "font/font2.ttf");   
String str = "abcdefghijk";
int len = str.length();
SpannableStringBuilder ss = new SpannableStringBuilder(str);
ss.setSpan (new CustomTypefaceSpan("", font1), 0, 2,Spanned.SPAN_EXCLUSIVE_INCLUSIVE);
ss.setSpan (new CustomTypefaceSpan("", font2), 2, 5,Spanned.SPAN_EXCLUSIVE_INCLUSIVE);
ss.setSpan (new CustomTypefaceSpan("", Typeface.DEFAULT), 5, len,Spanned.SPAN_EXCLUSIVE_INCLUSIVE);
textview.setText(ss);

注意:
setSpan(Object what, int start, int end, int flags)的參數:
字符的位置從0開始計數

  • int start:開始位置(以0爲基數)
  • int end:從start開始一共(end-start)個字符,即實際結束位置爲(end-1)
  • int flags:有2個值SPAN_EXCLUSIVE_INCLUSIVESPAN_EXCLUSIVE_EXCLUSIVE,使用中沒發現有什麼區別。O.O

參考:
1.android中用Spannable在TextView中設置超鏈接、顏色、字體
2.How set Spannable object font with custom font

“通知欄單詞”無法正確顯示音標

可以通過tickerText.setSpan(new StyleSpan(Typeface.BOLD_ITALIC), 0, tickerText.length(), Spannable.SPAN_INCLUSIVE_EXCLUSIVE);設置加粗或斜體,但用自定義CustomTypefaceSpan時無效。未找到解決辦法。
最後發現,其實手機是可以直接顯示音標的。。(直接複製音標比如setTicker("[əˈsɪst]"),手機上可以正常顯示!!!)
看來是源數據的編碼問題@_@
又找了幾份單詞excel文件,終於找到個可以正常顯示的了。下載單詞音標釋義版excel
換了數據後,一切正常顯示。。也不用替換字體了~

SQLite數據庫

記得之前有做筆記的,但是竟然找不到了。。
以前在實習公司用過Navicat,但是啓動特別緩慢(現在看來是電腦問題—_—),加上網上也有人說Navicat臃腫然後推薦了一堆別的軟件,今天之前都用的SQLiteStudio,實話說,不太好用。剛剛新的excel文件雖然在excel裏音標是正常的,但導出到txt音標又亂碼了,更換字體無效!!!於是只好安裝Navicat,可以直接導入excel數據,發現在我的電腦並不卡—_—果斷棄用SQLiteStudio。所以這麼曲折都是爲什麼。。。

2015-9-7

新增時間字段,類型選擇TEXT,在下方默認欄裏寫(datetime('now','localtime')) (注意最外要有括號),即可自動添加當前時區的時間。若默認CURRENT_TIMESTAMP,則時區爲GMT。
參考:
1.sqlite database default time value ‘now’
2.Sqlite: CURRENT_TIMESTAMP is in GMT, not the timezone of the machine
3.How to enter function values in tables ?

Button的點擊和父控件的衝突

1.在“懸浮窗單詞”SmallFloatWindowView中,一個Button用來播放單詞讀音,一個TextView顯示單詞。
最初是對“懸浮窗單詞”的SmallFloatWindowView進行onTouchEvent監聽,對Button進行OnClickListener監聽,但這樣Button無法移動,Textview可以移動。
改爲:button.setOnTouchListener監聽onTouch行爲,在onTouch裏執行button的操作。
2.雖然解決了問題,但2個監聽有很多重複代碼,只是處理點擊有所不同。
將兩者都設置setOnTouchListener,複寫onTouch

@Override
public boolean onTouch(View v, MotionEvent ev) {
    switch (ev.getAction()) {  
    case MotionEvent.ACTION_DOWN:  
        xInView = ev.getX();  
        yInView = ev.getY();  
        xDownInScreen = ev.getRawX();  
        yDownInScreen = ev.getRawY();  
        xInScreen = ev.getRawX();  
        yInScreen = ev.getRawY();  
        break;  
    case MotionEvent.ACTION_MOVE:  
        xInScreen = ev.getRawX();  
        yInScreen = ev.getRawY();  
        updateViewPosition();  
        break;  
    case MotionEvent.ACTION_UP:  
        if (xDownInScreen == xInScreen && yDownInScreen == yInScreen) {
            switch (v.getId()) {
            case R.id.small_float_window_layout:
                Log.d(VIEW_LOG_TAG, "點擊了float window");
                break;
            case R.id.small_float_window_play_btn:
                Log.d(VIEW_LOG_TAG, "點擊了btn");
                break;
            default:
                break;
            }
        }  
        break;  
    default:  
        break;  
    }
    return true;
}  

注意:最後要返回true,否則只能監聽到btn的行爲。
參考:MotionEvent.ACTION_UP on Textview

2015-9-8

RecyclerView添加PopMenu菜單

在ViewHolder裏配置:
1.setOnClickListener

public ViewHolder( View v ) {  
        super(v);  
        this.view = v;
        v.setOnLongClickListener(this);
} 

2.在OnLongClickListener創建PopupMenu

@Override
public boolean onLongClick(View v) {
    if ( v == this.view ) {
        PopupMenu popMenu = new PopupMenu(v.getContext(), v);
        popMenu.inflate(R.menu.wordbook_context_menu);
        popMenu.setOnMenuItemClickListener(this);
        popMenu.show();
    }
    return false;
}

3.在OnMenuItemClickListener給menu items添加操作

@Override
public boolean onMenuItemClick(MenuItem item) {
    switch (item.getItemId()) {
    case R.id.wordbook_context_menu_edit:
        Toast.makeText(view.getContext(),tvWord.getText().toString()+item.getTitle(), Toast.LENGTH_SHORT).show();
        break;
    case R.id.wordbook_context_menu_delete:
        Toast.makeText(view.getContext(),tvWord.getText().toString()+item.getTitle(), Toast.LENGTH_SHORT).show();
        break;
    case R.id.wordbook_context_menu_addto:
        Toast.makeText(view.getContext(),tvWord.getText().toString()+item.getTitle(), Toast.LENGTH_SHORT).show();
        break;
    default:
        break;
    }
    return true;
}

參考:Adding context menus to RecyclerView items

2015-9-9

SQLite數據庫

獲取所有表名

db = wordsDbHelper.getReadableDatabase();
List<String> tableList = new ArrayList<String>();
Cursor cursor = db.rawQuery("SELECT name FROM sqlite_master WHERE type='table' AND name!='android_metadata' order by name", null);
while(cursor.moveToNext()){
    tableList.add(cursor.getString(0));
}

參考:How to select all tables names instead android_metadata android SQLite

表重命名

public static void alterTableName(String oldName, String newName) {
    if ( ! getTableList().contains(newName) ) {
        db = wordsDbHelper.getReadableDatabase();
        db.execSQL("ALTER TABLE " + oldName + " RENAME TO " + newName + ";");
        db.close();
    }
}

RecyclerView詳細使用

參考:RecyclerView使用介紹

RecyclerView Adapter更新數據集

public void updateList(List<WordCls> newList) {
    this.wordsList = newList;
    notifyDataSetChanged();
}

AlertDialog對話框

參考:Android的AlertDialog詳解

2015-9-10

RecyclerView添加點擊事件,並獲取item position

參考:
1.RecyclerView使用介紹
2.Easy Implementation of RecyclerView custom onItemClickListener

RecyclerView水平滑動類似viewpager——繼承SnappingRecyclerView

參考:
1.SnappingRecyclerView.java
2.Snappy scrolling in RecyclerView

RecyclerView.Adapter根據viewType動態加載不同的item佈局

在Adapter裏
1.設置viewType

public static final int TYPE_VIEW_VERTICAL = 0;
public static final int TYPE_VIEW_HORIZON = 1;

@Override
public int getItemViewType(int position) {
    //也可以根據item的position設置不同的viewType
    return viewType;
}

public void setItemViewType(int viewType) {
    this.viewType = viewType;
}

2.一個item佈局對應一個ViewHolder
自定義一個BaseViewHolder,讓所有的ViewHolder繼承它。

public class BaseViewHolder extends RecyclerView.ViewHolder {
    public TextView tvWord;  
    public TextView tvPhonetic;  
    public TextView tvDefinition;  

    public BaseViewHolder(View v) {
        super(v);
    }
}

public class VerticalViewHolder extends BaseViewHolder {
    public ImageButton imgBtn;

    public VerticalViewHolder( View v) {  
        super(v); 

        tvWord = (TextView) v.findViewById(R.id.wordcard_vertical_tv_word);  
        tvPhonetic = (TextView) v.findViewById(R.id.wordcard_vertical_tv_phonetic);  
        tvDefinition = (TextView) v.findViewById(R.id.wordcard_vertical_tv_definition);  
        imgBtn = (ImageButton) v.findViewById(R.id.wordcard_vertical_imgbtn);  
    }
}  

public class HorizonViewHolder extends BaseViewHolder {

    public HorizonViewHolder( View v) {  
        super(v); 

        tvWord = (TextView) v.findViewById(R.id.wordcard_horizon_tv_word);  
        tvPhonetic = (TextView) v.findViewById(R.id.wordcard_horizon_tv_phonetic);  
        tvDefinition = (TextView) v.findViewById(R.id.wordcard_horizon_tv_definition);  
    }
}

3.根據viewType執行不同的操作

public class WordRecyclerViewAdapter extends RecyclerView.Adapter<WordRecyclerViewAdapter.**BaseViewHolder**> {
    ……

    @Override  
    public **BaseViewHolder** onCreateViewHolder( ViewGroup parent, int **viewType** )  
    {  
        final **BaseViewHolder** viewHolder;
        View v ;

        switch (**viewType**) {
        case TYPE_VIEW_HORIZON:
            v = LayoutInflater.from(parent.getContext()).inflate(R.layout.wordbook_item_horizontal_cardview, parent, false);  
            viewHolder = new HorizonViewHolder(v);
            break;
        case TYPE_VIEW_VERTICAL:
        default:
            v = LayoutInflater.from(parent.getContext()).inflate(R.layout.wordbook_item_vertical_cardview, parent, false);  
            viewHolder = new VerticalViewHolder(v);
            break;
        }

        return viewHolder;   
    }

    @Override  
    public void onBindViewHolder( **BaseViewHolder** baseViewHolder, int position ) {  
        WordCls wordCls = wordsList.get(position);  

        baseViewHolder.itemView.setTag(wordCls);

        baseViewHolder.tvWord.setText(wordCls.getWord());  
        baseViewHolder.tvPhonetic.setText(wordCls.getPhonetic());  
        baseViewHolder.tvDefinition.setText(wordCls.getDefinition());  

        switch (**baseViewHolder.getItemViewType()**) {
        case TYPE_VIEW_HORIZON:
            HorizonViewHolder horizonViewHolder = (HorizonViewHolder) baseViewHolder;
            ……
            break;
        case TYPE_VIEW_VERTICAL:
        default:
            VerticalViewHolder verticalViewHolder = (VerticalViewHolder) baseViewHolder;
            verticalViewHolder.imgBtn.setOnClickListener(new OnClickListener() {

                @Override
                public void onClick(View v) {
                }
            });
            ……
            break;
        }
    }  
}

參考:
1.How to Change the viewType of a RecyclerView item onClick
2.Recyclerview and handling different type of row inflation

2015-9-11

保存和恢復RecyclerView(Scroll)的精確滑動位置

1.scrollby

private int mScrollY;
private int mScrollYState;
//保存
private RecyclerView.OnScrollListener mTotalScrollListener = new RecyclerView.OnScrollListener() {
    @Override
    public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
        super.onScrolled(recyclerView, dx, dy);
        mScrollY += dy;
    }
};
//恢復
mScrollYState = mScrollY;
mRecyclerView.scrollBy(0, mScrollYState);

參考:Refreshing data in RecyclerView and keeping its scroll position

2.onSaveInstanceState

Parcelable recyclerViewState = recyclerView.getLayoutManager().onSaveInstanceState();//保存
recyclerView.getLayoutManager().onRestoreInstanceState(recyclerViewState);//恢復

直接存儲RecyclerView的InstanceState,但只能在fragment的生命週期裏使用
參考:

2015-9-15
3.#保存和恢復RecyclerView(Scroll)的精確滑動位置——改進版
嘗試了好幾天,終於想到了一種比較好的方式,能保存精確位置至本地,隨意切換列表數據也能恢復相應表的瀏覽位置,不過還是有一點點缺陷,第一頁的item會跳動一下。

  • scrollToPosition是根據你的操作方向來判斷目標item是顯示在頂端還是顯示在底端的,手指上滑則顯示在底端,手指下滑則顯示在頂端。那麼在切換列表時,則是根據現在的currentPositionscrollToPosition(position)position來判斷滑動方向的。這樣需要記錄頂端和底端的2個偏移量。

  • 切換列表時,是先計算好要滑動的位置,纔會顯示視圖的,所以只能根據當前列表(切換前顯示的列表)的視圖來計算位置,切換後纔會滑到正確的位置。即scrollToPosition會滑到頂端還是底端要靠你自己判斷的。

  • RecyclerView的界面還未出現時,比如第一次打開fragment或者切換到新的數據列表時,findFirstVisibleItemPosition的值是-1,這時是無法獲得child View的。調試時發現,只有當手機上能看到RecyclerView時,才能獲取child view。並且getChildAt(int index)中的index是指child在當前RecyclerView視圖中顯示的item個數中的index,而不是針對整個dataset的,故只能獲取到正在顯示的某一個child的View。

  • 若切換時currentFirstVisiblePosition < savedFirstVisiblePosition,即RecyclerView需要將列表往上拉(手指上滑)以顯示下面position較大的部分,scrollToPosition會顯示在底端。
    但是當savedFirstVisiblePosition在其dataset中的位置在手機中顯示是在第一頁時,scrollToPosition是無法滑動到底端的。我想到的是scrollToPosition後,讓列表scrollBy(0, recyclerView.getHeight());往上滑一部分,再scrollToPosition();回來,這樣目標item就會顯示在頂端了,再用smoothScrollBy(0, dyTop);就可以了。這裏最後一步沒有用scrollBy是因爲沒有效,我也不知道爲什麼,可能不支持連續滑動多次吧,也可以將scrollBy放在handler裏,效果和smoothScrollBy差不多;但是這2種方法都會出現跳動的動畫。

private void saveRecyclerViewPosition(String tableName) {
    firstVisibleItemPosition = ((LinearLayoutManager)recyclerView.getLayoutManager()).findFirstVisibleItemPosition();

    savedFirstVisibleChild = recyclerView.getChildAt(0);
    //dy正,手指將列表往上拉
    //dy負,手指將列表往下拉
    dyTop = savedFirstVisibleChild.getHeight() - savedFirstVisibleChild.getBottom(); 
    dyBottom = recyclerView.getHeight() - savedFirstVisibleChild.getBottom();

    prefEditorSettings.putInt(KEY_RECYCLERVIEW_SCROLL_POSITION + tableName, firstVisibleItemPosition);
    prefEditorSettings.putInt(KEY_RECYCLERVIEW_SCROLL_DY_TOP + tableName, dyTop);
    prefEditorSettings.putInt(KEY_RECYCLERVIEW_SCROLL_DY_BOTTOM + tableName, dyBottom);
    prefEditorSettings.commit(); 
}

private void restoreRecyclerViewPosition(String tableName) {
    if ( recyclerView != null) {
        savedFirstVisiblePosition = prefSettings.getInt(KEY_RECYCLERVIEW_SCROLL_POSITION + tableName, 0);
        dyTop = prefSettings.getInt(KEY_RECYCLERVIEW_SCROLL_DY_TOP + tableName, 0);
        dyBottom = prefSettings.getInt(KEY_RECYCLERVIEW_SCROLL_DY_BOTTOM + tableName, 0);

        currentFirstVisiblePosition =  ((LinearLayoutManager)recyclerView.getLayoutManager()).findFirstVisibleItemPosition();

        recyclerView.scrollToPosition(savedFirstVisiblePosition);

        if(currentFirstVisiblePosition > -1) {  
            if (currentFirstVisiblePosition >= savedFirstVisiblePosition) { //savedFirstVisiblePosition在頂部
                recyclerView.scrollBy(0, dyTop);
            } else if (currentFirstVisiblePosition < savedFirstVisiblePosition){    //savedFirstVisiblePosition在底部
                if (savedFirstVisiblePosition > 4)
                    recyclerView.scrollBy(0, dyBottom);             
                else {  //第一頁的item用handler/smoothScrollBy會有跳轉動作顯示,暫時會找到合適的辦法
                    recyclerView.scrollBy(0, recyclerView.getHeight());
                    recyclerView.scrollToPosition(savedFirstVisiblePosition);
                    recyclerView.smoothScrollBy(0, dyTop);
                }
            } 
        } else {    //第一次打開,還未出現界面
            recyclerView.scrollToPosition(savedFirstVisiblePosition);
            recyclerView.scrollBy(0, dyTop);
        }
    }
}
  • 這個方法不受生命週期的限制,但在上述情況中視覺體驗不太好,所以可以跟方法2結合起來使用。

    private void saveRecyclerViewPosition(String tableName) {
        recyclerViewState = recyclerView.getLayoutManager().onSaveInstanceState();
        hmRecyclerViewState.put(tableName, recyclerViewState);
    
        firstVisibleItemPosition = ((LinearLayoutManager)recyclerView.getLayoutManager()).findFirstVisibleItemPosition();
    
        savedFirstVisibleChild = recyclerView.getChildAt(0);
        //dy正,手指將列表往上拉
        //dy負,手指將列表往下拉
        dyTop = savedFirstVisibleChild.getHeight() - savedFirstVisibleChild.getBottom(); 
        dyBottom = recyclerView.getHeight() - savedFirstVisibleChild.getBottom();
    
        prefEditorSettings.putInt(KEY_RECYCLERVIEW_SCROLL_POSITION + tableName, firstVisibleItemPosition);
        prefEditorSettings.putInt(KEY_RECYCLERVIEW_SCROLL_DY_TOP + tableName, dyTop);
        prefEditorSettings.putInt(KEY_RECYCLERVIEW_SCROLL_DY_BOTTOM + tableName, dyBottom);
        prefEditorSettings.commit();
    }
    
    private void restoreRecyclerViewPosition(String tableName) {
        recyclerViewState = hmRecyclerViewState.get(tableName);
        if (recyclerViewState != null) {
            recyclerView.getLayoutManager().onRestoreInstanceState(recyclerViewState);
        } else {
            if ( recyclerView != null) {
                savedFirstVisiblePosition = prefSettings.getInt(KEY_RECYCLERVIEW_SCROLL_POSITION + tableName, 0);
                dyTop = prefSettings.getInt(KEY_RECYCLERVIEW_SCROLL_DY_TOP + tableName, 0);
                dyBottom = prefSettings.getInt(KEY_RECYCLERVIEW_SCROLL_DY_BOTTOM + tableName, 0);
    
                currentFirstVisiblePosition =  ((LinearLayoutManager)recyclerView.getLayoutManager()).findFirstVisibleItemPosition();
    
                recyclerView.scrollToPosition(savedFirstVisiblePosition);
    
                if(currentFirstVisiblePosition > -1) {
                    if (currentFirstVisiblePosition >= savedFirstVisiblePosition) { //savedFirstVisiblePosition在頂部
                        recyclerView.scrollBy(0, dyTop);
                    } else if (currentFirstVisiblePosition < savedFirstVisiblePosition){    //savedFirstVisiblePosition在底部
                        if (savedFirstVisiblePosition > 4)
                            recyclerView.scrollBy(0, dyBottom);             
                        else {  //第一頁的item用handler/smoothScrollBy會有跳轉動作顯示,暫時會找到合適的辦法
                            recyclerView.scrollBy(0, recyclerView.getHeight());
                            recyclerView.scrollToPosition(savedFirstVisiblePosition);
                            recyclerView.smoothScrollBy(0, dyTop);
                        }
                    } 
                } else {    //第一次打開,還未出現界面
                    recyclerView.scrollToPosition(savedFirstVisiblePosition);
                    recyclerView.scrollBy(0, dyTop);
                }
            }
        }
    }
    

slidingmenu切換fragment時的優化

避免每次切換fragment時都重新加載view。
注意:一個activity只有一個actionbar。

1.SlidingmenuFragment

@Override
public void onListItemClick(ListView lv, View v, int position, long id) {
    newContentFragment = null;

    object = lv.getItemAtPosition(position);
    str=(String)object;

    getActivity().setTitle(str);
    actionBar.setDisplayShowCustomEnabled(false);

    if (str.matches(getResources().getString(R.string.center))){
        if (centerFragment == null)
            centerFragment = new CenterFragment();
        newContentFragment = centerFragment;
    } else if (str.matches(getResources().getString(R.string.home))){
        if (homeFragment == null)
            homeFragment = new HomeFragment(getActivity());
        newContentFragment = homeFragment;
    } else if (str.matches(getResources().getString(R.string.wordbook))){
        if (wordBookFragment == null)
            wordBookFragment = new WordBookFragment();
        newContentFragment = wordBookFragment;
        //加載自定義actionbar佈局,在wordBookFragment裏實現actionBar.setCustomView()後,在這裏設置爲true即可。
        actionBar.setDisplayShowCustomEnabled(true);
    } else if (str.matches(getResources().getString(R.string.BBS))){
        if (bbsFragment == null)
            bbsFragment = new BBSFragment();
        newContentFragment = bbsFragment;
    } else if (str.matches(getResources().getString(R.string.settings))){
        if (settingsFragment == null)
            settingsFragment = new SettingsFragment();
        newContentFragment = settingsFragment;
    }


    if (newContentFragment != null){
        switchFragment(newContentFragment);
    }
}

private void switchFragment(Fragment fragment) {
    if (getActivity() == null)
        return;

    if (getActivity() instanceof MainActivity) {
        MainActivity main = (MainActivity) getActivity();
        main.switchContent(fragment);
    } 
}

2.MainActivity

public void switchContent(Fragment newFragment) {
    if ( contentFragment == null || newFragment == null)
        return;

    FragmentTransaction transaction = getFragmentManager().beginTransaction();

    if (contentFragment != newFragment) {
        if (!newFragment.isAdded()) {
            // 隱藏當前的fragment,add下一個到Activity中
            transaction.hide(contentFragment).add(R.id.content_frame,newFragment).commit();
        } else {
           // 隱藏當前的fragment,顯示下一個
            transaction.hide(contentFragment).show(newFragment).commit();
        }
        contentFragment = newFragment;
    }

    //通過handler來避免滑動卡頓的情況
    handler.post(new Runnable() {

        @Override
        public void run() {
            sm.showContent();
        }
    });
}

參考:

2015-9-16

聯網解析JSON數據

本來想用金山的,但一直沒收到key,發現扇貝的查單詞不用key,就用了扇貝的API
1.在AsyncTask裏聯網並解析數據

class ParseJsonTask extends AsyncTask<String, Void, Boolean> {
    HorizonViewHolder horizonViewHolder;
    WordCls wordCls;
    int position;

    String definitionEN;
    String definitionCN;
    String audioUrlUS;

    Handler handler;    //用來傳值

    public ParseJsonTask(HorizonViewHolder horizonViewHolder,
            WordCls wordCls, int position, Handler handler) {
        super();
        this.horizonViewHolder = horizonViewHolder;
        this.wordCls = wordCls;
        this.position = position;
        this.handler = handler;
    }

    @Override
    protected void onPreExecute() {
        horizonViewHolder.progressBar.setVisibility(View.VISIBLE);
        super.onPreExecute();
    }

    @Override
    protected void onPostExecute(Boolean result) {
        Message msg = handler.obtainMessage();

        horizonViewHolder.progressBar.setVisibility(View.INVISIBLE);
        if (result) {
            wordCls.setDefinitionEN(definitionEN);
            wordCls.setDefinitionCN(definitionCN);
            wordCls.setAudioUrlUS(audioUrlUS);
            wordCls.setLoaded(true);
            WordsManager.addWordLoadInfo(tableName, wordCls);
            updateItem(position, wordCls);

            msg.what = position;
        }else {
            msg.what = 0;
            Log.i(wordCls.getWord(), "獲取數據失敗");
        }

        handler.sendMessage(msg);

        super.onPostExecute(result);
    }

    @Override
    protected Boolean doInBackground(String... params) {
        String path = "https://api.shanbay.com/bdc/search/?word=" + wordCls.getWord();
        try {
            URL url = new URL(path);
            Source source = new Source(url.openConnection());   //jericho-html-3.1.jar
            String jsonstr = source.toString();

            JSONObject jsonObj = new JSONObject(jsonstr);

            JSONObject data = jsonObj.getJSONObject("data");

            JSONObject defEN = data.getJSONObject("en_definition");
            definitionEN = defEN.getString("pos") + "." + defEN.getString("defn"); 

            JSONObject defCN = data.getJSONObject("cn_definition");
            definitionCN = defCN.getString("pos") + defCN.getString("defn"); 

            audioUrlUS = data.getString("us_audio");

            return true;
        } catch (Exception e) {
            Toast.makeText(mContext, "獲取數據失敗", Toast.LENGTH_SHORT).show();
            return false;
        }
    }
}

if ( ! wordCls.isLoaded() ) {
    Handler handler = new Handler(){

        @Override
        public void handleMessage(Message msg) {    根據AsyncTask的執行結果傳遞的值來判斷下一步操作
            if (msg.what == position)
                horizonViewHolder.tvHint.setVisibility(View.INVISIBLE);
            else
                horizonViewHolder.tvHint.setText("數據獲取失敗,請重試");
        }

    };
    ParseJsonTask parseJsonTask = new ParseJsonTask(horizonViewHolder, wordCls, position, handler);
    parseJsonTask.execute();
} 

參考:

2015-9-17

RecyclerView水平滑動類似viewpager——自定義方法實現

之前繼承的SnappingRecyclerView,水平狀態的時候,點擊item裏的TextView更新會出現錯位,有時還會跑到第一個位置去,並且感覺他的有點複雜。。
然後發現自己寫一個實現也並不難@_@ 我的每一個item都是全屏的卡片,所以這樣就可以了。

private void scrollToCenter(RecyclerView recyclerView) {
    firstVisibleItemPosition = ((LinearLayoutManager)recyclerView.getLayoutManager()).findFirstVisibleItemPosition();
    lastVisibleItemPosition = ((LinearLayoutManager)recyclerView.getLayoutManager()).findLastVisibleItemPosition();

    if ( firstVisibleItemPosition < lastVisibleItemPosition ) {
        recyclerViewWidth = recyclerView.getWidth();
        firstVisibleChild = recyclerView.getChildAt(0);
        firstChildVisibleWidth = firstVisibleChild.getRight();
        if ( firstChildVisibleWidth > ( recyclerViewWidth / 2 ) )
            recyclerView.smoothScrollToPosition(firstVisibleItemPosition);
        else if ( firstChildVisibleWidth < ( recyclerViewWidth / 2 ) )
            recyclerView.smoothScrollToPosition(lastVisibleItemPosition);
    }
}

@Override
public void onScrollStateChanged(final RecyclerView recyclerView, int newState) {
    super.onScrollStateChanged(recyclerView, newState);

    switch (newState) {
    case RecyclerView.SCROLL_STATE_IDLE:
        switch (wordCardAdapter.getItemViewType()) {
        case TYPE_VIEW_HORIZON:
            scrollToCenter(recyclerView);
            break;
        case TYPE_VIEW_VERTICAL:
        default:
            break;
        }
        break;
    case RecyclerView.SCROLL_STATE_DRAGGING:
        break;
    case RecyclerView.SCROLL_STATE_SETTLING:
        break;
 }
}

actionbar下拉列表——PopupWindow

View view = LayoutInflater.from(context).inflate(R.layout.popmenu, null);       
popupWindow = new PopupWindow(view, 100, LayoutParams.WRAP_CONTENT);
// 使其聚集
popupWindow.setFocusable(true);
// 設置允許在外點擊消失
popupWindow.setOutsideTouchable(true);
//刷新狀態(必須刷新否則無效)
popupWindow.update();
// 這個是爲了點擊“返回Back”也能使其消失,並且並不會影響你的背景
popupWindow.setBackgroundDrawable(new BitmapDrawable()); 

popupWindow.showAsDropDown(v); //設置顯示位置

參考:

2015-9-18

RecyclerView高度wrap_content無效

自定義MyLayoutManager

/*
 * Copyright 2015 serso aka se.solovyev
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 *
 * Contact details
 *
 * Email: [email protected]
 * Site:  http://se.solovyev.org
 */

package org.solovyev.android.views.llm;

import android.content.Context;
import android.graphics.Rect;
import android.support.v4.view.ViewCompat;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.view.View;

import java.lang.reflect.Field;

/**
 * {@link android.support.v7.widget.LinearLayoutManager} which wraps its content. Note that this class will always
 * wrap the content regardless of {@link android.support.v7.widget.RecyclerView} layout parameters.
 * <p/>
 * Now it's impossible to run add/remove animations with child views which have arbitrary dimensions (height for
 * VERTICAL orientation and width for HORIZONTAL). However if child views have fixed dimensions
 * {@link #setChildSize(int)} method might be used to let the layout manager know how big they are going to be.
 * If animations are not used at all then a normal measuring procedure will run and child views will be measured during
 * the measure pass.
 */
public class LinearLayoutManager extends android.support.v7.widget.LinearLayoutManager {

    private static boolean canMakeInsetsDirty = true;
    private static Field insetsDirtyField = null;

    private static final int CHILD_WIDTH = 0;
    private static final int CHILD_HEIGHT = 1;
    private static final int DEFAULT_CHILD_SIZE = 100;

    private final int[] childDimensions = new int[2];
    private final RecyclerView view;

    private int childSize = DEFAULT_CHILD_SIZE;
    private boolean hasChildSize;
    private int overScrollMode = ViewCompat.OVER_SCROLL_ALWAYS;
    private final Rect tmpRect = new Rect();

    @SuppressWarnings("UnusedDeclaration")
    public LinearLayoutManager(Context context) {
        super(context);
        this.view = null;
    }

    @SuppressWarnings("UnusedDeclaration")
    public LinearLayoutManager(Context context, int orientation, boolean reverseLayout) {
        super(context, orientation, reverseLayout);
        this.view = null;
    }

    @SuppressWarnings("UnusedDeclaration")
    public LinearLayoutManager(RecyclerView view) {
        super(view.getContext());
        this.view = view;
        this.overScrollMode = ViewCompat.getOverScrollMode(view);
    }

    @SuppressWarnings("UnusedDeclaration")
    public LinearLayoutManager(RecyclerView view, int orientation, boolean reverseLayout) {
        super(view.getContext(), orientation, reverseLayout);
        this.view = view;
        this.overScrollMode = ViewCompat.getOverScrollMode(view);
    }

    public void setOverScrollMode(int overScrollMode) {
        if (overScrollMode < ViewCompat.OVER_SCROLL_ALWAYS || overScrollMode > ViewCompat.OVER_SCROLL_NEVER)
            throw new IllegalArgumentException("Unknown overscroll mode: " + overScrollMode);
        if (this.view == null) throw new IllegalStateException("view == null");
        this.overScrollMode = overScrollMode;
        ViewCompat.setOverScrollMode(view, overScrollMode);
    }

    public static int makeUnspecifiedSpec() {
        return View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
    }

    @Override
    public void onMeasure(RecyclerView.Recycler recycler, RecyclerView.State state, int widthSpec, int heightSpec) {
        final int widthMode = View.MeasureSpec.getMode(widthSpec);
        final int heightMode = View.MeasureSpec.getMode(heightSpec);

        final int widthSize = View.MeasureSpec.getSize(widthSpec);
        final int heightSize = View.MeasureSpec.getSize(heightSpec);

        final boolean hasWidthSize = widthMode != View.MeasureSpec.UNSPECIFIED;
        final boolean hasHeightSize = heightMode != View.MeasureSpec.UNSPECIFIED;

        final boolean exactWidth = widthMode == View.MeasureSpec.EXACTLY;
        final boolean exactHeight = heightMode == View.MeasureSpec.EXACTLY;

        final int unspecified = makeUnspecifiedSpec();

        if (exactWidth && exactHeight) {
            // in case of exact calculations for both dimensions let's use default "onMeasure" implementation
            super.onMeasure(recycler, state, widthSpec, heightSpec);
            return;
        }

        final boolean vertical = getOrientation() == VERTICAL;

        initChildDimensions(widthSize, heightSize, vertical);

        int width = 0;
        int height = 0;

        // it's possible to get scrap views in recycler which are bound to old (invalid) adapter entities. This
        // happens because their invalidation happens after "onMeasure" method. As a workaround let's clear the
        // recycler now (it should not cause any performance issues while scrolling as "onMeasure" is never
        // called whiles scrolling)
        recycler.clear();

        final int stateItemCount = state.getItemCount();
        final int adapterItemCount = getItemCount();
        // adapter always contains actual data while state might contain old data (f.e. data before the animation is
        // done). As we want to measure the view with actual data we must use data from the adapter and not from  the
        // state
        for (int i = 0; i < adapterItemCount; i++) {
            if (vertical) {
                if (!hasChildSize) {
                    if (i < stateItemCount) {
                        // we should not exceed state count, otherwise we'll get IndexOutOfBoundsException. For such items
                        // we will use previously calculated dimensions
                        measureChild(recycler, i, widthSize, unspecified, childDimensions);
                    } else {
                        logMeasureWarning(i);
                    }
                }
                height += childDimensions[CHILD_HEIGHT];
                if (i == 0) {
                    width = childDimensions[CHILD_WIDTH];
                }
                if (hasHeightSize && height >= heightSize) {
                    break;
                }
            } else {
                if (!hasChildSize) {
                    if (i < stateItemCount) {
                        // we should not exceed state count, otherwise we'll get IndexOutOfBoundsException. For such items
                        // we will use previously calculated dimensions
                        measureChild(recycler, i, unspecified, heightSize, childDimensions);
                    } else {
                        logMeasureWarning(i);
                    }
                }
                width += childDimensions[CHILD_WIDTH];
                if (i == 0) {
                    height = childDimensions[CHILD_HEIGHT];
                }
                if (hasWidthSize && width >= widthSize) {
                    break;
                }
            }
        }

        if (exactWidth) {
            width = widthSize;
        } else {
            width += getPaddingLeft() + getPaddingRight();
            if (hasWidthSize) {
                width = Math.min(width, widthSize);
            }
        }

        if (exactHeight) {
            height = heightSize;
        } else {
            height += getPaddingTop() + getPaddingBottom();
            if (hasHeightSize) {
                height = Math.min(height, heightSize);
            }
        }

        setMeasuredDimension(width, height);

        if (view != null && overScrollMode == ViewCompat.OVER_SCROLL_IF_CONTENT_SCROLLS) {
            final boolean fit = (vertical && (!hasHeightSize || height < heightSize))
                    || (!vertical && (!hasWidthSize || width < widthSize));

            ViewCompat.setOverScrollMode(view, fit ? ViewCompat.OVER_SCROLL_NEVER : ViewCompat.OVER_SCROLL_ALWAYS);
        }
    }

    private void logMeasureWarning(int child) {
        if (BuildConfig.DEBUG) {
            Log.w("LinearLayoutManager", "Can't measure child #" + child + ", previously used dimensions will be reused." +
                    "To remove this message either use #setChildSize() method or don't run RecyclerView animations");
        }
    }

    private void initChildDimensions(int width, int height, boolean vertical) {
        if (childDimensions[CHILD_WIDTH] != 0 || childDimensions[CHILD_HEIGHT] != 0) {
            // already initialized, skipping
            return;
        }
        if (vertical) {
            childDimensions[CHILD_WIDTH] = width;
            childDimensions[CHILD_HEIGHT] = childSize;
        } else {
            childDimensions[CHILD_WIDTH] = childSize;
            childDimensions[CHILD_HEIGHT] = height;
        }
    }

    @Override
    public void setOrientation(int orientation) {
        // might be called before the constructor of this class is called
        //noinspection ConstantConditions
        if (childDimensions != null) {
            if (getOrientation() != orientation) {
                childDimensions[CHILD_WIDTH] = 0;
                childDimensions[CHILD_HEIGHT] = 0;
            }
        }
        super.setOrientation(orientation);
    }

    public void clearChildSize() {
        hasChildSize = false;
        setChildSize(DEFAULT_CHILD_SIZE);
    }

    public void setChildSize(int childSize) {
        hasChildSize = true;
        if (this.childSize != childSize) {
            this.childSize = childSize;
            requestLayout();
        }
    }

    private void measureChild(RecyclerView.Recycler recycler, int position, int widthSize, int heightSize, int[] dimensions) {
        final View child;
        try {
            child = recycler.getViewForPosition(position);
        } catch (IndexOutOfBoundsException e) {
            if (BuildConfig.DEBUG) {
                Log.w("LinearLayoutManager", "LinearLayoutManager doesn't work well with animations. Consider switching them off", e);
            }
            return;
        }

        final RecyclerView.LayoutParams p = (RecyclerView.LayoutParams) child.getLayoutParams();

        final int hPadding = getPaddingLeft() + getPaddingRight();
        final int vPadding = getPaddingTop() + getPaddingBottom();

        final int hMargin = p.leftMargin + p.rightMargin;
        final int vMargin = p.topMargin + p.bottomMargin;

        // we must make insets dirty in order calculateItemDecorationsForChild to work
        makeInsetsDirty(p);
        // this method should be called before any getXxxDecorationXxx() methods
        calculateItemDecorationsForChild(child, tmpRect);

        final int hDecoration = getRightDecorationWidth(child) + getLeftDecorationWidth(child);
        final int vDecoration = getTopDecorationHeight(child) + getBottomDecorationHeight(child);

        final int childWidthSpec = getChildMeasureSpec(widthSize, hPadding + hMargin + hDecoration, p.width, canScrollHorizontally());
        final int childHeightSpec = getChildMeasureSpec(heightSize, vPadding + vMargin + vDecoration, p.height, canScrollVertically());

        child.measure(childWidthSpec, childHeightSpec);

        dimensions[CHILD_WIDTH] = getDecoratedMeasuredWidth(child) + p.leftMargin + p.rightMargin;
        dimensions[CHILD_HEIGHT] = getDecoratedMeasuredHeight(child) + p.bottomMargin + p.topMargin;

        // as view is recycled let's not keep old measured values
        makeInsetsDirty(p);
        recycler.recycleView(child);
    }

    private static void makeInsetsDirty(RecyclerView.LayoutParams p) {
        if (!canMakeInsetsDirty) {
            return;
        }
        try {
            if (insetsDirtyField == null) {
                insetsDirtyField = RecyclerView.LayoutParams.class.getDeclaredField("mInsetsDirty");
                insetsDirtyField.setAccessible(true);
            }
            insetsDirtyField.set(p, true);
        } catch (NoSuchFieldException e) {
            onMakeInsertDirtyFailed();
        } catch (IllegalAccessException e) {
            onMakeInsertDirtyFailed();
        }
    }

    private static void onMakeInsertDirtyFailed() {
        canMakeInsetsDirty = false;
        if (BuildConfig.DEBUG) {
            Log.w("LinearLayoutManager", "Can't make LayoutParams insets dirty, decorations measurements might be incorrect");
        }
    }
}

參考:RecyclerView高度隨Item自適應

2015-9-19

ListView高度

在ListView下面有一個Button,ListView設置高度爲wrap_content,當ListView高度超出屏幕時,Button就不顯示了。
解決:
給ListView增加

android:layout_weight="1" 

2015-9-21

SearchView

按返回鍵關閉SearchView

@Override
public void onBackPressed() {
    if ( searchView != null ) {
        if (!searchView.isIconified()) {
            searchView.setIconified(true);
        } else {
            super.onBackPressed();
        }
    }
}

參考:How do I close a SearchView programmatically?

點擊外部關閉SearchView

public void setupUI(View view) {

    if(!(view instanceof SearchView)) {

        view.setOnTouchListener(new OnTouchListener() {

            public boolean onTouch(View v, MotionEvent event) {
                searchMenuItem.collapseActionView();
                return false;
            }

        });
    }

    //If a layout container, iterate over children and seed recursion.
    if (view instanceof ViewGroup) {

        for (int i = 0; i < ((ViewGroup) view).getChildCount(); i++) {

            View innerView = ((ViewGroup) view).getChildAt(i);

            setupUI(innerView);
        }
    }
}

參考:how to make searchview loose focus and collapse when clicked elsewhere on activity

2015-9-22

Sqlite判斷記錄是否存在

Cursor cursor = db.query(……);
if (cursor.moveToNext())
    return true;
else
    return false;

Sqlite模糊查詢

String[] columns = { COLUMN_WORD, COLUMN_DEFINITION };
String selection = COLUMN_DEFINITION + " like ? "; 
String[] selectionArgs = new String[]{ "%" + value + "%" };

db = wordsDbHelper.getReadableDatabase();
Cursor cursor = db.query(tableName, columns, selection, selectionArgs, null, null, COLUMN_WORD);

Bundle實現Android Activity間消息的傳遞

Intent intent = new Intent();  
intent.setClass(TestBundle.this, Target.class);  
Bundle mBundle = new Bundle();  
mBundle.putString("Data", "ray'blog");//壓入數據  
intent.putExtras(mBundle);  
startActivity(intent);  

Bundle bundle = getIntent().getExtras();    
String data=bundle.getString("Data");//取出數據

Volley

JsonRequest

RequestQueue mQueue = Volley.newRequestQueue(context);  
JsonObjectRequest jsonObjectRequest = new JsonObjectRequest("http://m.weather.com.cn/data/101010100.html", null,  
    new Response.Listener<JSONObject>() {  
        @Override  
        public void onResponse(JSONObject response) {  
            Log.d("TAG", response.toString());  
        }  
    }, new Response.ErrorListener() {  
        @Override  
        public void onErrorResponse(VolleyError error) {  
            Log.e("TAG", error.getMessage(), error);  
        }  
    }); 
mQueue.add(jsonObjectRequest);  

2015-9-24

半透明背景

res/values/color.xml

<color name="transparent_background">#50000000</color>

#5000000前兩位是透明的效果參數從00—99(透明—不怎麼透明),後6位是顏色的設置

參考:android 成長 UI 學習之 Activity 透明,半透明效果的設置transparent

圓角邊框

drawable目錄裏定義一個corners_bg.xml:

<?xml version="1.0" encoding="utf-8"?>    
<shape xmlns:android="http://schemas.android.com/apk/res/android">      
    <solid android:color="@color/translucent_background" />     //填充顏色,這裏設置的半透明 
    <corners android:radius="3dp" />    //圓角弧度
</shape>   

引用:

android:background="@drawable/corners_bg"

參考:【Android】Android佈局中實現圓角邊框

播放音頻

  1. MediaPlayer
  • 適合播放較大文件,文件應該存儲在SD卡上,而不是在資源文件裏
  • 資源佔用量較高,延遲時間較長。
  • 不支持多個音頻同時播放

(1)從資源文件中播放

MediaPlayer player = new MediaPlayer.create(this,R.raw.test);
player.stare();

(2)從文件系統播放

MediaPlayer player = new MediaPlayer();
String path = "/sdcard/test.mp3";
player.setDataSource(path);
player.prepare();
player.start();

(3)從網絡播放

  • 通過URI的方式:

    String path="http://**************.mp3";     //音頻的網絡地址
    Uri uri = Uri.parse(path);
    MediaPlayer player = new MediaPlayer.create(this,uri);
    player.start();
    
    • 通過設置數據源的方式:

      MediaPlayer player = new MediaPlayer.create();
      String path="http://**************.mp3";          //音頻的網絡地址
      player.setDataSource(path);
      player.prepare();
      player.start();
      

      參考:Android中的音頻播放(MediaPlayer和SoundPool)

2.SoundPool
低延遲播放,適合播放實時音實現同時播放多個聲音,如遊戲中炸彈的爆炸音等小資源文件,此類音頻比較適合放到資源文件夾 res/raw下和程序一起打成APK文件

  • SoundPool 使用音效池的概念來管理多個短促的音效;
  • cpu資源佔用量低和反應延遲小;
  • 支持自行設置的品質、音量、播放比率等參數;
  • 異步線程,佔用資源少,可以同時合成多種音效;

修改ActionBar icon大小

  • API < 17,res/values/styles.xml :

    <item name="android:actionButtonStyle">@style/ActionButtonStyle</item>
    
    <style name="ActionButtonStyle" parent="@android:style/Widget.Holo.Light.ActionButton">
        <item name="android:minWidth">0dip</item>
        <item name="android:paddingLeft">0dip</item>
        <item name="android:paddingRight">0dip</item>                  
    </style>
    
  • API > 17,res/values-v17/styles.xml :

    <item name="android:actionButtonStyle">@style/ActionButtonStyle</item>
    
    <style name="ActionButtonStyle" parent="@android:style/Widget.Holo.Light.ActionButton">
        <item name="android:minWidth">0dip</item>
        <item name="android:paddingStart">0dip</item>
        <item name="android:paddingEnd">0dip</item>                  
    </style>
    

    參考:Is there a way to reduce the spacing between the Action Item Icons on Action Bar?

修改actionbar的高度

style.xml:

<resources xmlns:android="http://schemas.android.com/apk/res/android">
    <style name="AppTheme" parent="@android:style/Theme.Holo.Light">
        <item name="android:actionBarSize">30dp</item>
    </style>
</resources>

manifest.xml:

<application
    android:label="@string/app_name"
    android:theme="@style/AppTheme"
    android:icon="@drawable/ic_launcher"
 >

參考:ActonBar介紹-修改actionbar的高度

修改SearchView樣式

文字顏色

輸入的文字:

int id = searchView.getContext().getResources().getIdentifier("android:id/search_src_text", null, null);
TextView textView = (TextView) searchView.findViewById(id);
textView.setTextColor(Color.WHITE);

參考:Android中SearchView修改字體顏色

hint提示文字:

searchView.setQueryHint(Html.fromHtml("<font color = #999999>" + getResources().getString(R.string.search_input) + "</font>"));

參考:關於SearchView的一些小細節

自定義樣式

使用SearchViewFormatter
用法

new SearchViewFormatter()
        .setSearchBackGroundResource(R.drawable.my_bg)
        .setSearchIconResource(R.drawable.my_ic, true, false) //true to icon inside edittext, false to outside
        .setSearchVoiceIconResource(R.drawable.my_ic)
        .setSearchTextColorResource(R.color.my_color)
        .setSearchHintColorResource(R.color.my_color)
        .setSearchCloseIconResource(R.drawable.my_ic)
        .setInputType(InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS)
        .format(mSearchView);

修改Cursor顏色

@Override
public boolean onCreateOptionsMenu(Menu menu) {
    getMenuInflater().inflate(R.menu.entity_list_actions, menu);
    final SearchView searchView = (SearchView) menu.findItem(R.id.search).getActionView();
    final int textViewID = searchView.getContext().getResources().getIdentifier("android:id/search_src_text",null, null);
    final AutoCompleteTextView searchTextView = (AutoCompleteTextView) searchView.findViewById(textViewID);
    try {
        Field mCursorDrawableRes = TextView.class.getDeclaredField("mCursorDrawableRes");
        mCursorDrawableRes.setAccessible(true);
        mCursorDrawableRes.set(searchTextView, 0); //This sets the cursor resource ID to 0 or @null which will make it visible on white background
    } catch (Exception e) {}
    return super.onCreateOptionsMenu(menu);
}

參考:Changing the cursor color in SearchView without ActionBarSherlock

ActionBar返回上一個Activity

在此Activity裏設置

ActionBar mActionBar = getActionBar();
mActionBar.setDisplayHomeAsUpEnabled(true);//show back button

在Manifest.xml文件中設置這個Activity的parentActivity

<activity
    android:parentActivityName=".MainActivity"
    android:name=".BackActionBarActivity"
    android:launchMode="singleTask"
    android:label="@string/title_activity_back_action_bar">
    <meta-data
        android:name="android.support.PARENT_ACTIVITY"
        android:value=".MainActivity"/>
</activity>

參考:Android ActionBar 返回上一個Activity

修改指定Activity的Actionbar

styles.xml:

<style name="CustomActivityTheme" parent="AppTheme">
    <item name="android:homeAsUpIndicator">@drawable/custom_home_as_up_icon</item>
</style>

Manifest.xml:

<activity
        android:name="com.example.CustomActivity"
        android:theme="@style/CustomActivityTheme" >
</activity>

參考:Change the actionbar homeAsUpIndicator Programamtically

2015-9-25

RecyclerView添加分割線

使用RecyclerView-FlexibleDivider

RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recyclerview);
recyclerView.addItemDecoration(
        new HorizontalDividerItemDecoration.Builder(this)
            .color(Color.RED)
            .size(getResources().getDimensionPixelSize(R.dimen.divider))
            .margin(getResources().getDimensionPixelSize(R.dimen.leftmargin),
                    getResources().getDimensionPixelSize(R.dimen.rightmargin))
            .build());

參考:RecyclerView-FlexibleDivider——控制RecyclerView項目分割的Android類庫

2015-9-26

Volley加載網絡圖片

ImageRequest的用法

  1. 創建一個RequestQueue對象。
  2. 創建一個Request對象。
  3. 將Request對象添加到RequestQueue裏面。

    RequestQueue mQueue = Volley.newRequestQueue(context);
    ImageRequest imageRequest = new ImageRequest(
        "http://developer.android.com/images/home/aw_dac.png",
        new Response.Listener<Bitmap>() {
            @Override
            public void onResponse(Bitmap response) {
                imageView.setImageBitmap(response);
            }
        }, 0, 0, Config.RGB_565, new Response.ErrorListener() {
            @Override
            public void onErrorResponse(VolleyError error) {
                imageView.setImageResource(R.drawable.default_image);
            }
        });
    mQueue.add(imageRequest);
    

ImageRequest的構造函數接收六個參數,第一個參數就是圖片的URL地址,這個沒什麼需要解釋的。第二個參數是圖片請求成功的回調,這裏我們把返回的Bitmap參數設置到ImageView中。第三第四個參數分別用於指定允許圖片最大的寬度和高度,如果指定的網絡圖片的寬度或高度大於這裏的最大值,則會對圖片進行壓縮,指定成0的話就表示不管圖片有多大,都不會進行壓縮。第五個參數用於指定圖片的顏色屬性,Bitmap.Config下的幾個常量都可以在這裏使用,其中ARGB_8888可以展示最好的顏色屬性,每個圖片像素佔據4個字節的大小,而RGB_565則表示每個圖片像素佔據2個字節大小。第六個參數是圖片請求失敗的回調,這裏我們當請求失敗時在ImageView中顯示一張默認圖片。

ImageLoader

ImageLoader也可以用於加載網絡上的圖片,並且它的內部也是使用ImageRequest來實現的,不過ImageLoader明顯要比ImageRequest更加高效,因爲它不僅可以幫我們對圖片進行緩存,還可以過濾掉重複的鏈接,避免重複發送請求。

  1. 創建一個RequestQueue對象。
  2. 創建一個ImageLoader對象。
  3. 獲取一個ImageListener對象。
  4. 調用ImageLoader的get()方法加載網絡上的圖片。

    ImageLoader imageLoader = new ImageLoader(mQueue, new ImageCache() {

    @Override
    public void putBitmap(String url, Bitmap bitmap) {
    }
    
    @Override
    public Bitmap getBitmap(String url) {
        return null;
    }
    

    });
    ImageListener listener = ImageLoader.getImageListener(imageView,

    R.drawable.default_image, R.drawable.failed_image);
    

    //imageLoader.get(“http://img.my.csdn.net/uploads/201404/13/1397393290_5765.jpeg“, listener);
    imageLoader.get(“http://img.my.csdn.net/uploads/201404/13/1397393290_5765.jpeg“, listener);

參考:Android Volley完全解析(二),使用Volley加載網絡圖片

Textview文字末尾拼接帶本地圖片背景文字

textView = (TextView) findViewById(R.id.text);

ImageGetter imageGetter = new ImageGetter() {
  @Override
  public Drawable getDrawable(String source) {
      int resId = Integer.parseInt(source);
      Drawable drawable = MainActivity.this.getResources()
              .getDrawable(resId);
      drawable.setBounds(0, 0, drawable.getIntrinsicWidth(),
              drawable.getIntrinsicHeight());
      return drawable;
  }
};

textView.setText(Html.fromHtml("我要添加一個<img src=\""+R.drawable.ic_launcher+"\">,看到了嗎?", imageGetter, null));

文字和背景合併插入正文中

自定義TextDrawable,將文字內容傳入,用canvas將文字和繪製的圓角矩形合併(本地圖片同理)
@Override
public void draw(Canvas canvas) {
paint.setColor(Color.RED);
rectF.set(padding, -height-linsSpaceExtra, padding+rectWidth, -linsSpaceExtra);
canvas.drawRoundRect(rectF, height/2, height/2, paint);
//canvas.drawRect(padding, -height-linsSpaceExtra, padding+rectWidth, -linsSpaceExtra, paint);
int baseline = (int) (rectF.top + (rectF.bottom - rectF.top - paint.getFontMetrics().bottom + paint.getFontMetrics().top) / 2 - paint.getFontMetrics().top)-2;
paint.setColor(Color.WHITE);
canvas.drawText(text, rectF.centerX(), baseline, paint);
}
參考:Textview文字末尾拼接帶本地圖片背景文字

2015-10-5

獲取當前日期

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
SimpleDateFormat函數語法:
  
  G 年代標誌符
  y 年
  M
  d
  h 時 在上午或下午 (1~12)
  H 時 在一天中 (0~23)
  m
  s 秒
  S 毫秒
  E 星期
  D 一年中的第幾天
  F 一月中第幾個星期幾
  w 一年中第幾個星期
  W 一月中第幾個星期
  a 上午 / 下午 標記符 
  k 時 在一天中 (1~24)
  K 時 在上午或下午 (0~11)
  z 時區
SimpleDateFormat myFmt=new SimpleDateFormat("yyyy年MM月dd日 HH時mm分ss秒");
SimpleDateFormat myFmt1=new SimpleDateFormat("yy/MM/dd HH:mm"); 
SimpleDateFormat myFmt2=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");//等價於now.toLocaleString()
SimpleDateFormat myFmt3=new SimpleDateFormat("yyyy年MM月dd日 HH時mm分ss秒 E ");
SimpleDateFormat myFmt4=new SimpleDateFormat(
        "一年中的第 D 天 一年中第w個星期 一月中第W個星期 在一天中k時 z時區");
Date now=new Date();
System.out.println(myFmt.format(now));
System.out.println(myFmt1.format(now));
System.out.println(myFmt2.format(now));
System.out.println(myFmt3.format(now));
System.out.println(myFmt4.format(now));
System.out.println(now.toGMTString());
System.out.println(now.toLocaleString());
System.out.println(now.toString());

參考:SimpleDateFormat使用詳解

2015-10-7

SQLite索引

索引可大幅減少掃描表所需的時間。 請遵照以下準則:
索引中的列順序會影響性能。 WHERE 子句通常使用的列應該放在前面,然後放置 ORDER BY 子句通常使用的列。
對於包含檢索的數據的列,創建覆蓋索引。
避免重複索引。 SQLite 數據庫引擎將自動爲具有 UNIQUE 或 PRIMARY KEY 限制的列創建索引。

SQLite複製表數據到已存在的表中

insert into tagTable select * from sourceTable;  
//tagTable     目標數據庫  
//sourceTable  源數據庫  

參考:Sqlite 將一張表的數據複製到另一張表中

SQLite刪除字段

sqlite中是不支持刪除列操作的,所以網上alter table table_name drop column col_name這個語句在sqlite中是無效的,而替代的方法可以如下:

1.根據原表創建一張新表
2.刪除原表
3.將新表重名爲舊錶的名稱

示例例子如下

1.創建一張舊錶Student,包含id(主碼),name, tel

create table student (

id integer primary key,

name text,

tel text

)

2.給舊錶插入兩個值

insert into student(id,name,tel) values(101,"Jack","110")

insert into student(id,name,tel) values(102,"Rose","119")

3.接下來我們刪除電話這個列,首先根據student表創建一張新表teacher

create table teacher as select id,name from student

可以看到tel這一列已經沒有了

4.然後我們刪除student這個表

drop table if exists student

5.將teacher這個表重命名爲student

alter table teacher rename to student

結果演示:

select * from student order by name descdesc降序, asc升序)

這樣就可以得到我們想要的結果了。

參考:Sqlite刪除列方法

2015-10-9

MediaPlayer從頭播放

```
if ( playerSentence != null ) {
    if ( playerSentence.isPlaying() ) {
        playerSentence.seekTo(0);
    } else {
        playerSentence.start();
    }
}
```            

參考:android裏用MediaPlayer,當音樂現在正在播放時,點擊按鈕是如何讓音樂從頭播放

判斷數據庫表是否存在

Cursor c=db.rawQuery("SELECT count(*) FROM sqlite_master WHERE type='table' AND name='要查詢的表名'", null);  
if (c.getInt(0)==0) {  
    return false;  
} 

參考:判斷sqlite數據庫中表是否存在的方法

2015-10-10

禁止橫屏豎屏切換

AndroidManifest.xml的需要禁止轉向的Activity配置中加入android:screenOrientation="portrait"屬性即可(landscape是橫向,portrait是縱向)
參考:Android禁止橫屏豎屏切換

2015-10-11

文件下載

OkHttp

參考:

從路徑獲取文件名

//      方法一:  

    File tempFile =new File( fName.trim());  
    String fileName = tempFile.getName();            
    System.out.println("fileName = " + fileName);  

//      方法二:    
    String fName = fName.trim();    
    String fileName = fName.substring(fName.lastIndexOf(File.separator)+1);            
    System.out.println("fileName = " + fileName);  

//      方法三:    
    String fName = fName.trim();    
    String temp[] = fName.split("\\\\"); /**split裏面必須是正則表達式,"\\"的作用是對字符串轉義*/    
    String fileName = temp[temp.length-1];  

參考:3種Java從文件路徑中獲取文件名的方法

2016-02-28

CardView適配

  • 不同 SDK 版本(低於 Lollipop 21)上的邊距(Margin)效果

在低版本中設置了 CardElevation 之後 CardView 會自動留出空間供陰影顯示,而 Lollipop 之後則需要手動設置 Margin 邊距來預留空間。
因此,我們需要自定義一個 dimen 作爲 CardView 的 Margin 值:

創建 /res/value 和 /res/value-v21 資源文件夾於項目對應 Module 目錄下,前者放置舊版本/通用的資源文件(瞭解的可以跳過),後者放置 21 及更高 SDK 版本的資源文件。

在 value 內的 dimen.xml 創建一個 Dimension ( 屬性),隨便命個名(如 xxx_card_margin)並填入數值 0dp

接着在 value-v21 文件夾內的 dimen.xml 創建名字相同的 Dimension,並填入你期望的預留邊距(一般和 CardElevation 陰影大小相同)

最後,在你佈局中的 CardView 中設置 android:layout_margin="@dimen/xxx_card_margin"

這樣就解決了低版本中邊距過大或者視覺效果不統一的問題了。

參考:關於使用 CardView 開發過程中要注意的細節

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章