여러 앱에서 필수적인 기능인 로그인 기능을 구현하겠습니다. 버터나이프라는 좋은 모듈을 사용할 것입니다. 버터나이프는 안드로이드에서 view에 쉽게 접근할 수 있게 해 줍니다. 

butterknife : http://jakewharton.github.io/butterknife/


1. 레이아웃 불러오기

먼저 레이아웃에서 로그인 아이디값, 비밀번호값 등을 가져오기 전에 그 값들을 저장할 데이터 모델과 그 모델을 처리할 apiService를 불러오겠습니다.


import com.example.keepair.myapplication.apiservice.LoginApiService;
import com.example.keepair.myapplication.helper.Constants;
import com.example.keepair.myapplication.loginhelper.ReferSharedPreference;
import com.example.keepair.myapplication.model.LoginData;
import com.example.keepair.myapplication.model.MyKey;
cs

여기서 ReferSharedPreference는 나중에 살펴볼 것입니다. 지금은 자동 로그인을 위한 SharedPreference 라고 알고 넘어가면 될 것 같습니다. SharedPreference는 안드로이드에서 유용하게 사용되는 저장 도구입니다. 앞서 말한대로 버터나이프 모듈을 쓸 것이니 그것도 가져와줍니다.


import butterknife.Bind;
import butterknife.ButterKnife;
import butterknife.OnClick;
cs

그 다음 값을 담을 변수들을 정합니다.


    MyKey keygiven;
    LoginApiService loginApiService;
    @Bind(R.id.usernameTextField)
    EditText mUsernameTextField;
    @Bind(R.id.passwordTextField)
    EditText mPasswordTextField;
    @Bind(R.id.loginButton)
    ImageView mLoginButton;
    @Bind(R.id.btn_start_RegistrationActivity)
    ImageView mStartRegistrationButton;
    @Bind(R.id.layout_Login)
    RelativeLayout mLayoutLogin;
    ReferSharedPreference mSavedUserInfo;
cs

@Bind가 butterknife의 역할입니다. @Bind를 통해 길던 코드가 매우 짧아졌습니다. butterknife를 쓰는 사람들은 이 간결함을 선호합니다.


2. 함수 지정

onCreate를 설정하겠습니다. onCreate는 생성될 때의 옵션을 의미합니다. 화면이 만들어질 때부터 기능할 수 있는 부분을 정하게 됩니다.


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_login);
        ButterKnife.bind(this);
 
        mSavedUserInfo = new ReferSharedPreference(getApplicationContext());
 
        mUsernameTextField.setText(mSavedUserInfo.getValue("SavedUserName"""));
        mPasswordTextField.setText(mSavedUserInfo.getValue("SavedPassword"""));
    }
cs

mSavedUserInfo가 없다면 새로운 값을 가져오게 되는 것입니다. 새로운 값을 가져올 때는 레이아웃에서 값을 가져와보고, 레이아웃에서 값이 존재하지 않는다면 "" 값을 가져옵니다. 빈 텍스트를 가져오게 됩니다. 저장된 값들은 SharedPreference에서 존재합니다.

 

등록 버튼을 눌렀을 때 새로운 RegisterActivity가 생성되도록 intent를 만들겠습니다.

로그인 화면이 있고 로그인 화면에서 등록 버튼을 눌러 사용자 등록을 위한 화면을 생성하는 구조입니다.


    @OnClick(R.id.btn_start_RegistrationActivity)
    public void onClickToStartRegistration(View view){
        Intent intent = new Intent(LoginActivity.this, RegistrationActivity.class);
        startActivity(intent);
    }
cs

로그인 버튼을 구현합니다. @onClick을 butterknife를 이용해 만듭니다. @onClick도 butterknife에서 제공되는 유용한 도구입니다.


    @OnClick(R.id.loginButton)
    public void onClick(View view) {
    }
cs

이제 여기 안에 내용을 채우면 됩니다.

우선 사용자가 작성한 값들을 레이아웃으로부터 가져옵니다.


        final String givenUserName = mUsernameTextField.getText().toString();
        String givenEmail = "";
        final String givenPassword = mPasswordTextField.getText().toString();
        LoginData loginData = new LoginData(givenUserName, givenPassword, givenEmail);
cs


Okhttp3를 이용해서 로깅을 조절합니다. header랑 body를 지정합니다. 이걸 통해 http가 어떻게 통신되는지 확인할 수 있습니다.


        HttpLoggingInterceptor logging = new HttpLoggingInterceptor();
        logging.setLevel(HttpLoggingInterceptor.Level.BODY);
        logging.setLevel(HttpLoggingInterceptor.Level.HEADERS);
 
cs

로그인에 이용할 Retrofit을 불러올 것입니다.  Retrofit는 나중에 살펴볼 비동기 통신 도구입니다. 사전작업을 하겠습니다.

client라는 변수에 okHttpClient() 값을 넣어줍니다.


        OkHttpClient client = new OkHttpClient();
        OkHttpClient.Builder builder = new OkHttpClient.Builder();
        builder.interceptors().add(logging);
 
        client = builder.build();
 
        Retrofit retrofit = new Retrofit.Builder()
                .client(client)
                .addConverterFactory(GsonConverterFactory.create())
                .baseUrl(Constants.BASE_URL)
                .build();
cs


이제 직접적으로 서버와 통신할 Call부분을 만듭니다.

Call부분은 이렇게 성공한 경우와 실패한 경우를 나눠서 대응할 수 있습니다. 서버와의 통신에 성공하면 response.isSuccessful()이 호출되고 그렇지 않다면 onFailure()가 호출될 것입니다.

 
        loginApiService = retrofit.create(LoginApiService.class);
        Call<MyKey> getget = loginApiService.getget(loginData);
        getget.enqueue(new Callback<MyKey>() {
            @Override
            public void onResponse(Call<MyKey> call, Response<MyKey> response) {
                if (response.isSuccessful()) {
                }
                else {
                    Toast.makeText(getApplicationContext(),"Failed", Toast.LENGTH_LONG).show();
                }
            }
            @Override
            public void onFailure(Call<MyKey> call, Throwable t) {
            }
        });
cs

실패한다면 별 반응이 없어도 되지만 성공한다면 로그인 이후의 반응을 작성해야합니다.


                    keygiven = response.body();
 
                    Toast.makeText(getApplicationContext(),"We waited you, " + givenUserName, Toast.LENGTH_LONG).show();
 
                    mSavedUserInfo.put("SavedUserName", givenUserName);
                    mSavedUserInfo.put("SavedPassword", givenPassword);
 
                    ReferSharedPreference givenToken = new ReferSharedPreference(getApplicationContext());
                    givenToken.put("Token""Token "+ keygiven.getKey());
                    Intent intent = new Intent(LoginActivity.this, MainActivity.class);
                    intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
                    intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                    startActivity(intent);
cs


이렇게 하면 통신에 성공했을 때 로그인을 하여 startActivity를 실행하게 됩니다.

실패하게 되면 간단히 Toast만 띄워줍니다.


                    Toast.makeText(getApplicationContext(),"Failed", Toast.LENGTH_LONG).show();
cs


LoginActivity를 간단하게 만들어 보았습니다.

1. Infinite Scroll이란

Infinite Scroll은 무한스크롤, 즉 유튜브나 페이스북, 트위터 등에서 스크롤이 끊임없이 이어지는 것을 말합니다.
브라우저 같은 경우 자바스크립트로 구현할 수 있겠고 안드로이드도 그런 식으로 만드는 것이 가능하겠으나 많이 사용되고 있으므로 여러가지 툴이 존재합니다. 그 중 구글이 제공하는 Recycler View를 이용해서 만들어보겠습니다.

2. Recycler View 이용


우선 Recycler View 라고 구글에서 제공해주는 좋은 재료가 있습니다.


import android.support.v7.widget.RecyclerView;
cs


그 다음 Listener를 달아줍니다.


public abstract class EndlessScrollListener extends RecyclerView.OnScrollListener {
}
cs


이렇게 RecyclerView.onScrollListener를 extends하여 구현합니다.

여기에 작업을 해주면 됩니다. 세부적인 옵션을 설정하겠습니다.


    private int visibleThreshold = 2;
    private int currentPage = 1;
    private int previousTotalItemCount = 0;
    private boolean loading = true;
    private int startingPageIndex = 1;
    private RecyclerView.LayoutManager mLayoutManager;
cs


변수의 이름대로 옵션을 설정했습니다.

그 다음엔 Scroll에 필요한 Listener를 등록하기 위해 layoutManager를 작성합니다.


    protected EndlessScrollListener(LinearLayoutManager layoutManager) {
        this.mLayoutManager = layoutManager;
    }
    
    public EndlessScrollListener(GridLayoutManager layoutManager) {
        this.mLayoutManager = layoutManager;
        visibleThreshold = visibleThreshold * layoutManager.getSpanCount();
    }
    
    public EndlessScrollListener(StaggeredGridLayoutManager layoutManager) {
        this.mLayoutManager = layoutManager;
        visibleThreshold = visibleThreshold * layoutManager.getSpanCount();
    }
cs

이렇게 여러가지 매니저를 처리하도록 합니다. visibleThreshold와 layoutManager의 getSpanCount()를 이용해서 visibleThreshold에 다시 값을 지정합니다.


그 다음 스크롤 후 마지막 항목 차례에서 벌어질 이벤트를 처리합니다. 이 부분을 통해 무한으로 스크롤되는 유저 경험을 제공합니다. 마지막으로 보이게 되는 아이템과 그 아이템의 위치를 이용해서 for 문을 구성하고 maxSize 변수에 값을 넣어 이벤트를 만들 것입니다.


    @Contract(pure = true)
    private int getLastVisibleItem(int[] lastVisibleItemPositions) {
        int maxSize = 0;
        for (int i = 0; i < lastVisibleItemPositions.length; i++) {
            if (i == 0) {
                maxSize = lastVisibleItemPositions[i];
            } else if (lastVisibleItemPositions[i] > maxSize) {
                maxSize = lastVisibleItemPositions[i];
            }
        }
        return maxSize;
    }
cs


3. 마지막 항목에서의 이벤트

마지막 항목까지 스크롤 되었을 때의 이벤트를 조금 더 세부적으로 설정합니다. lastVisibleItemPosition 변수를 만들어서 처리합니다.


    @Override
    public void onScrolled(RecyclerView view, int dx, int dy) {
        int lastVisibleItemPosition = 0;
        int totalItemCount = mLayoutManager.getItemCount();
        
        if (mLayoutManager instanceof StaggeredGridLayoutManager) {
            int[] lastVisibleItemPositions = ((StaggeredGridLayoutManager) mLayoutManager).findLastVisibleItemPositions(null);
            // get maximum element within the list
            lastVisibleItemPosition = getLastVisibleItem(lastVisibleItemPositions);
        } else if (mLayoutManager instanceof GridLayoutManager) {
            lastVisibleItemPosition = ((GridLayoutManager) mLayoutManager).findLastVisibleItemPosition();
        } else if (mLayoutManager instanceof LinearLayoutManager) {
            lastVisibleItemPosition = ((LinearLayoutManager) mLayoutManager).findLastVisibleItemPosition();
        }
        
        if (totalItemCount < previousTotalItemCount) {
            this.currentPage = this.startingPageIndex;
            this.previousTotalItemCount = totalItemCount;
            if (totalItemCount == 0) {
                this.loading = true;
            }
        }
        if (loading && (totalItemCount > previousTotalItemCount)) {
            loading = false;
            previousTotalItemCount = totalItemCount;
        }
        if (!loading && (lastVisibleItemPosition + visibleThreshold) > totalItemCount) {
            currentPage++;
            onLoadMore(currentPage, totalItemCount, view);
            loading = true;
        }
    }
cs


이렇게 한 후엔 스크롤이 반복적으로 reset될 state를 설정하면 됩니다.


    public void resetState() {
        this.currentPage = this.startingPageIndex;
        this.previousTotalItemCount = 0;
        this.loading = true;
    }
 
cs


그 다음에 마무리로 더 불러오기를 구현합니다. 여러가지 처리를 한 끝에 자동적으로 스크롤할 콘텐츠를 불러오게 됩니다. 사용자는 끊임없이 스크롤되는 것처럼 느끼게 됩니다.


    public abstract void onLoadMore(int page, int totalItemsCount, RecyclerView view);
cs


이렇게 끝맺을 수 있습니다.

지난 포스트에서 비율을 정하는 부분이 있었습니다.


        int width = opts.outWidth;
        int height = opts.outHeight;
 
        float sampleRatio = getSampleRatio(width, height);
cs


이 부분을 더 자세히 알아보겠습니다. getSampleRatio는 width와 height 값을 받는 함수입니다. java에서 기본으로 제공하는 함수가 아니므로 설명이 더 필요합니다.


1. getSampleRatio() 


    private float getSampleRatio(int width, int height) {
    }
cs


이 함수를 채워넣을 것입니다.


        final int targetWidth = 1280;
        final int targetheight = 1280;
        float ratio;
cs


우선 이렇게 인자를 형성합니다. 흔히 확인할 수 있는 화면 해상도인 1280 * 720 에서의 1280 입니다.


        if (width > height) {
            // 가로
            if (width > targetWidth) {
                ratio = (float) width / (float) targetWidth;
            } else ratio = 1f;
        } else {
            // 세로
            if (height > targetheight) {
                ratio = (float) height / (float) targetheight;
            } else ratio = 1f;
        }
cs


이렇게 가로와 세로의 경우의 수를 따져줍니다. 모바일 화면이니 1280이 그 기준이 됩니다.


    private File createFileFromBitmap(Bitmap bitmap) throws IOException {
        File newFile = new File(getActivity().getFilesDir(), makeImageFileName());
        FileOutputStream fileOutputStream = new FileOutputStream(newFile);
        bitmap.compress(Bitmap.CompressFormat.PNG, 100, fileOutputStream);
        fileOutputStream.close();
        return newFile;
    }
cs


이렇게 File에다가 Bitmap을 이용해서 함수를 만들어줍니다. getSampleRatio는 단순히 비율만을 결정하므로 파일을 가져와주는 과정이 필요합니다. FileOutPutStram(newFile)을 이용한 다음에 주어진 비트맵을 편집할 수 있도록 bitmap.compress를 사용해줍니다.


2. makeImageFileName()


    private String makeImageFileName() {
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyyMMdd_hhmmss");
        Date date = new Date();
        String strDate = simpleDateFormat.format(date);
        return strDate + ".png";
    }
cs


여기서 FileName을 설정해줄 수 있습니다. format도 정해줄 수 있습니다. 이름을 정함과 동시에 포맷까지 결정하는 함수입니다. 이미지파일을 다룰 것이므로 png 확장자를 사용했습니다. SimpleDateFormat은 java에서 제공해줍니다.


import java.text.SimpleDateFormat;
import java.util.Date;
cs


이걸 이용하면 됩니다. 날짜 관련 자료들을 텍스트나 원하는 형식으로 편집할 때 유용하게 쓰입니다.

1. 준비물 

Bitmap을 이용할 것입니다. 그러므로 Bitmap관련 모듈들을 import하고 getBitmapFromUri를 새로 만듭니다.


1
2
3
4
5
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;  
      private Bitmap getBitmapFromUri(Uri uri) throws IOException {
    }
cs


그 다음에 FileDescriptor를 import 하고 새로 FileDescriptor를 써 줍니다.


1
2
3
4
5
6
import java.io.FileDescriptor;
import java.io.FileOutputStream;
import java.io.IOException;
        ParcelFileDescriptor parcelFileDescriptor =
                getActivity().getContentResolver().openFileDescriptor(uri, "r");
        FileDescriptor fileDescriptor = parcelFileDescriptor.getFileDescriptor();
cs


2. 비트맵 파일 편집


파일을 가져온 다음엔 BitmapFactory를 이용해서 세부 옵션을 설정합니다.


        BitmapFactory.Options opts = new BitmapFactory.Options();
        opts.inJustDecodeBounds = true;
        BitmapFactory.decodeFileDescriptor(fileDescriptor, null, opts);
cs


옵션은 따로 정해줄 수 있습니다.


        int width = opts.outWidth;
        int height = opts.outHeight;
 
        float sampleRatio = getSampleRatio(width, height);
 
        opts.inJustDecodeBounds = false;
        opts.inSampleSize = (int) sampleRatio;
 
cs


그 후에 Bitmap 스트림을 닫아줍니다.


        Bitmap resizedBitmap = BitmapFactory.decodeFileDescriptor(fileDescriptor, null, opts);
        Log.d("Resizing", "Resized Width / Height : " + resizedBitmap.getWidth() + "/" + resizedBitmap.getHeight());
        parcelFileDescriptor.close();
        return resizedBitmap;
cs


3. 이미지 파일에 부여될 텍스트 편집

Bitmap으로 불러온 파일에 이름을 지정할 것이므로 아래의 코드를 activity에서 원하는 자리에 위치시킵니다. 부모레이아웃과 자식레이아웃을 설정해서 작업해봤습니다.


    public void onAddField(View v) {
        LayoutInflater inflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        final View rowView = inflater.inflate(R.layout.field, null);
        parentLinearLayout.addView(rowView, parentLinearLayout.getChildCount());
        int count = parentLinearLayout.getChildCount();
        TextView result_field = findViewById(R.id.result);
        result_field.setText(Integer.toString((count-1)));
        View view = parentLinearLayout.getChildAt(count-1);
        if(view instanceof EditText){
            EditText last = (EditText)view;
            last.setSelection(0);
        }
    }
cs


그 다음 이름을 사용자로부터 받은 후 등록여부를 재차 확인하는 버튼을 구현합니다.


    @Override
    public void onBackPressed() {
        AlertDialog.Builder builder = new AlertDialog.Builder(this);
        builder.setTitle("You can Save your title");
        builder.setMessage("re-check your message?");
        builder.setPositiveButton("Enter"new DialogInterface.OnClickListener() {
            public void onClick(DialogInterface dialog, int which) {
                dialog.dismiss();
                finish();
            }
        });
        builder.setNegativeButton("Deny"new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                dialog.dismiss();
            }
        });
        AlertDialog alert = builder.create();
        alert.show();
    }
}
cs


이렇게 긍정 버튼과 부정 버튼을 설정했습니다. 사용자가 Enter와 Deny 둘 중 하나를 선택하여 이름을 지정할 수 있습니다.

1. 준비물

GeoDjango는 django project의 일종으로 위치 데이터를 다룰 수 있게 해줍니다. 위치 데이터는 보통 구글맵을 이용한 좌표 데이터를 의미한다고 봐도 됩니다. 그 외에도 3d나 2d 좌표를 이용한 계산 등의 기능도 가능케 합니다.

GeoDjango : https://docs.djangoproject.com/en/2.0/ref/contrib/gis/


models.py 에서 설정하는 여러가지 방법이 있지만 PointField를 쓰기로 했습니다. 


class Post(models.Model):
    
    author = models.ForeignKey(User, related_name='related_postwriter')
    text = models.TextField(null = True, blank = True)
    image = models.ImageField(null = True, blank = True, upload_to='gogo')
    created_date = models.DateTimeField(
        default=timezone.now
        )
    point = models.PointField(blank=False, null=False)
    #여기서 PointField를 쓴다. PointField는 좌표를 저장하기에 좋습니다.
 
    def __str__(self):              # __unicode__ on Python 2
        return self.author
cs


이젠 PointField의 정보를 views.py에서 다룰 차례입니다.

그 전에 admin.py에 Post 모델을 등록합니다.


from .models import Post
 
admin.site.register(Post)
cs


2. views.py 설정


그리고 views.py 에서 import는 이렇게 해 줍니다.


from django.contrib.gis.geos import GEOSGeometry
from django.contrib.gis.measure import D
cs


ViewSet class를 만듭니다.


class PostViewSet(viewsets.ModelViewSet):
    serializer_class = PostSerializer
    permission_classes = [IsAuthenticated]
    queryset = Post.objects.all()
cs


django-rest-framework 를 쓰고 있으니 import할 것이 있습니다. django-rest-framework를 더욱 편리하게 해줍니다.

django-rest-framework는 django 에서 RESTful API를 구성하기 쉽게 해 줍니다. 많이 사용되는 패키지입니다.

django-rest-framework : http://www.django-rest-framework.org/


from rest_framework import filters, viewsets, generics
 
cs


이러면 viewsets.ModelViewSet을 쓸 수 있습니다.


from blog.models import Post
 
cs

모델을 호출하지 않으면 동작하지 않습니다.


    def get_queryset(self) :
        lat = self.request.GET.get('user_lat''15')
        lon = self.request.GET.get('user_lon''13')
        userpoint = GEOSGeometry('POINT(' + lon + ' ' + lat + ')', srid=4326)
        self.result = []
        return self.result
cs


쿼리셋을 불러오는 일부분입니다. 여기에 옵션을 조금 더 추가합니다.


        while i<20:
            elasped_minutes = datetime.now() - timedelta(minutes=10*i)
            list_i = Post.objects.filter(point__distance_lte = (userpoint, D(m=i*500))).order_by("-created_date")
            if len(self.result) > 50:
                self.result = self.result[:50]
                break
            i += 1
cs



이렇게 되면 일정 거리에 있는 결과들을 결과값으로 가져오게 됩니다. 500m 단위로 필터를 반복하게 구성했습니다.


3. 작성자와 request.user 일치시키기

작성자는 글쓴 사람으로 등록되게 하겠습니다.

serializer의 self값에서 request요소를 받아내고 있음을 이용합니다.


    def perform_create(self, serializer):
        serializer.save(author=self.request.user)
cs


django-pipeline은 배포에 있어 static 파일들의 업데이트를 손쉽게 해주는 장점이 좋은 파이썬 패키지입니다.


1. 설치

pip install django-pipeline
cs


pip를 이용해서 설치합니다.


INSTALLED_APPS = (
    'pipeline',
)
cs


settings.py 에 앱을 추가합니다.


STATICFILES_STORAGE = 'pipeline.storage.PipelineCachedStorage'
cs
STATICFILES_FINDERS = (
    'django.contrib.staticfiles.finders.FileSystemFinder',
    'django.contrib.staticfiles.finders.AppDirectoriesFinder',
    'pipeline.finders.PipelineFinder',
)
cs


따로 static 파일들의 저장경로와 static 파일을 찾는 finder를 설정해줍니다. 기본 설정이 끝납니다.


2. 세부 설정


그다음 staticfiles_dir 을 참조하여 다른 설정들도 해야 합니다.

staticfiles_dir 과 비슷한 구조입니다.


PIPELINE = {
    'new_css': {
        'css': {
            'source_filenames': (
              'css/css_1.css',
              'css/css_help/*.css',
              'css/css_2.css'
            ),
            'output_filename''css/new_css_output.css'
        },
    },
    'new_js': {
        'js': {
            'source_filenames': (
              'js/jquery.js',
              'js/js_help/*.js',
              'js/js_1.js',
            ),
            'output_filename''js/new_js_output.js',
        }
    }
}
cs


이 것을 settings.py에 추가합니다. django-pipeline은 이 정보를 바탕으로 작업을 수행합니다. 일반 collectstatic 과정과 비슷하지만 다른 점은 pipeline을 통해 만들어진 static파일들은 각기 특이한 이름을 갖게 된다는 것입니다. 그러므로 브라우저가 파일을 로드할 시에 이름이 중복되기 때문에 추가로 로드하지 않는 경우를 막을 수 있습니다.


3. 사용법

그 후 template파일을 정리하면 됩니다. template파일에서도 불러오는 방법이 존재합니다.


{% load pipeline %}
{% new_css 'css' %}
{% new_js 'js' %}
cs

이렇게 불러오게 됩니다.

pipeline을 이용해서 collectstatic이 진행된 결과를 불러오게 됩니다.


여기서 settings.py에서 PIPELINE={} 안의 이름들을 지정할 때 혼동되기 쉽습니다.

그러므로 나중에 설정할 때는 편리하게 알아보기 쉬운 이름으로 설정하는 것이 좋을 것 같습니다.


1. 준비물

Dialog를 사용할 것입니다. Dialog를 위해 이런 코드를 써야 합니다.


import android.content.DialogInterface;
import android.content.Intent;
 
import android.support.v4.content.ContextCompat;
import android.support.v7.app.AlertDialog;
import android.support.v7.app.AppCompatDialogFragment;    
 
    private void openDialog() {
    }
cs


이렇게 하면 Dialog가 열립니다.

Dialog 안에도 inflater를 써서 콘텐츠를 가져옵니다.


        LayoutInflater inflater = LayoutInflater.from(getContext());
        View subView = inflater.inflate(R.layout.dialog_layout, null);
cs


View도 가져옵니다. View를 가져올 시엔 inflater를 사용합니다. getContext()는 액티비티의 속성을 가져오기 위해 사용합니다.


        final EditText subEditText = (EditText) subView.findViewById(R.id.dialogEditText);
 
cs


EditText는 유저가 할 말을 전달하는 역할을 수행합니다.


그 외 기타 코드들 입니다.


        subEditText.setSelection(subEditText.length());
        AlertDialog.Builder builder = new AlertDialog.Builder(new ContextThemeWrapper(getContext(), R.style.myDialog));
        builder.setTitle("Text to Post");
        builder.setView(subView);
        AlertDialog alertDialog = builder.create();
cs


Dialog는 builder로 create()해줘야 합니다.


2. 긍정, 부정 버튼

긍정 버튼은 이렇게 구현합니다.


        builder.setPositiveButton("Ok", new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialogInterface, int i) {
                mEditTextdialogOpenField.setText(subEditText.getText().toString());
            }
        });
cs


이러면 긍정적으로 버튼이 작동합니다. 질문에 긍정적으로 답했을 때 코드들이 실행되는 구조입니다.

반대로 부정 버튼은 이렇게 합니다.


        builder.setNegativeButton("Cancel"new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialogInterface, int i) {
                Snackbar.make(getView(), "Cancel", Snackbar.LENGTH_LONG).show();
            }
        });
cs


이러면 버튼이 부정적으로 동작하게 됩니다.


        builder.show();
 
cs


애써 만든 코드에 show()를 더해줘야 정상적으로 동작합니다.

1. 준비물

이미지 갤러리를 열 수 있게 버튼을 만들어서 가져오겠습니다.


        mGetImageFromGalleryButton = (ImageView) view.findViewById(R.id.btn_getgallery);
 
cs


여기에 setOnClickListener를 달아줍니다.


        mGetImageFromGalleryButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
            }
        });
cs


onClick() 안에 원하는 것을 채워주겠습니다.


2. 권한 획득 후 실행


                if(ActivityCompat.checkSelfPermission(getActivity(),
                        Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED)
                {
                    requestPermissions(
                            new String[]{Manifest.permission.READ_EXTERNAL_STORAGE},
                            2000);
                }
                else {
                    startGallery();
                }
cs


이렇게 하면 권한 획득을 구현하게 됩니다. 권한이 있으면 자동으로 startGallery() 함수를 호출합니다.

지금까지 구현한 코드의 마지막에 써줘야 할 것이 있습니다.


        return view;
cs


이렇게 마무리 해주지 않으면 코드가 정상적으로 작동하지 않습니다.

갤러리를 열 때 필요한 코드가 또 있습니다.


    private void startGallery() {
        Intent cameraIntent = new Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
        cameraIntent.setType("image/*");
        if (cameraIntent.resolveActivity(getActivity().getPackageManager()) != null) {
            startActivityForResult(cameraIntent, 1000);
        }
    }
cs


이 부분의 코드가 있어야 Intent를 이용해서 열 수 있습니다. Intent 는 새 화면이라고 생각하시면 됩니다.


1. 준비물

기본적인 구조는 다음과 같습니다.


            @Override
            public void onClick(View view) {
                if (returnUri != null) {
                    if (!mEditTextdialogOpenField.getText().toString().equals("")) {
                    }
                    else {
                        Snackbar.make(getView(), "You have to say any words", Snackbar.LENGTH_LONG).show();
                    }
                }
                else {
                    Snackbar.make(getView(), "You have to get any image from gallery", Snackbar.LENGTH_LONG).show();
                }
            }
cs


if 구문에 Snackbar를 등록한 형태입니다. 이제 이 안을 채울 것입니다. Dialog로부터 입력받은 텍스트가 존재하면 실행되는 구문입니다. 그렇지 않다면 Snackbar로 에러를 표시합니다.


2. 내용물


    
                        Bitmap bitmap = null;
                        try {
                            bitmap = getBitmapFromUri(returnUri);
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
cs


이 것으로 비트맵을 가져옵니다. uri를 통해 비트맵을 열 수 있습니다. 비트맵은 그림파일이라고 생각하시면 됩니다.


                        File imageFile = null;
                        try {
                            imageFile = createFileFromBitmap(bitmap);
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
cs


여기는 파일을 가져오는 부분을 구현했습니다. 이 파일을 이제 비동기통신으로 연결하겠습니다.

가져온 텍스트를 Retrofit에 실어주는 부분입니다. okHttpClient를 사용할 때엔 builder를 호출해서 생성해야 합니다. 그리고 interceptor()를 builder에서 호출하여 현재 activity의 view.getContext() 속성을 넣어줍니다.


                        String givenText = mEditTextdialogOpenField.getText().toString();
                        OkHttpClient client = new OkHttpClient();
                        OkHttpClient.Builder builder = new OkHttpClient.Builder();
                        builder.interceptors().add(new AddCookiesInterceptor(view.getContext()));
                        client = builder.build();
                        Retrofit retrofit = new Retrofit.Builder()
                                .client(client)
                                .addConverterFactory(GsonConverterFactory.create())
                                .baseUrl(Constants.BASE_URL)
                                .build();
cs


okHttpClient의 구현 방법에 주의하시기 바랍니다.

사전에 만들어 놓은 api 서비스가 있습니다. 이 api 서비스를 이용해서 사진을 다루겠습니다.


                        PostApiService postApiService = retrofit.create(PostApiService.class);
                        RequestBody requestFile =
                                RequestBody.create(MediaType.parse("multipart/form-data"), imageFile);
                        MultipartBody.Part body =
                                MultipartBody.Part.createFormData("image", makeImageFileName(), requestFile);
 
cs


좌표는 int혹은 string으로 다뤄질 수 있습니다. 이번엔 String 변수에 담겠습니다. 사용하실 때 편한 방법으로 값을 저장해두면 불러오거나 다시 저장할 때 유용합니다.


                        ReferSharedPreference preferenceCoordinates = new ReferSharedPreference(getContext());
                        final String lat = preferenceCoordinates.getValue("Lat", "13");
                        final String lon = preferenceCoordinates.getValue("Lon", "15");
                        String pointString = "{ \"longitude\": \""+lon+"\",     \"latitude\": \""+lat+"\" } ";
 
cs


Request할 Body 부분을 구현합니다. http통신이므로 request가 존재합니다.


                        RequestBody coordinatePoint =
                                RequestBody.create(
                                        MediaType.parse("multipart/form-data"), pointString);
                        RequestBody textToPost =
                                RequestBody.create(
                                        MediaType.parse("multipart/form-data"), givenText);
 
cs


이제 Retrofit를 이용해서 Call을 구현하면 됩니다. call은 서버에 요청하는 과정입니다.


                        Call<ResponseBody> call = postApiService.uploadFile(body, coordinatePoint, textToPost);
                        call.enqueue(new Callback<ResponseBody>() {
                            @Override
                            public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {
                                Snackbar.make(getView(), "Success, posted at \n" + lat + "  , " + lon , Snackbar.LENGTH_LONG).show();
 
                            }
                            @Override
                            public void onFailure(Call<ResponseBody> call, Throwable t) {
                            }
                        });
cs


그 다음 ImageView를 정리합니다.


                        mImageview.setImageBitmap(null);
                        returnUri = null;
                        mEditTextdialogOpenField.setText("");
cs


이러면 전송 후 이미지뷰가 깨끗해집니다. 이미지가 계속 남아있으면 다음 요청시에 에러가 발생할 수 있기 때문입니다.

1. 준비물

Glide, 서비스할 API와 Model들을 가져옵니다.


import com.bumptech.glide.Glide;
import com.example.keepair.myapplication.apiservice.LoginApiService;
import com.example.keepair.myapplication.apiservice.PostApiService;
import com.example.keepair.myapplication.helper.Constants;
import com.example.keepair.myapplication.loginhelper.AddCookiesInterceptor;
import com.example.keepair.myapplication.loginhelper.ReferSharedPreference;
import com.example.keepair.myapplication.model.Point;
import com.example.keepair.myapplication.model.PostData;
cs


버터나이프를 쓸 것입니다. 버터나이프는 뷰를 가져올 때 편리한 도구입니다.

그리고 okhttp3를 써서 json 데이터를 다루기 편하게 할 것입니다.


import butterknife.Bind;
import butterknife.OnClick;
import okhttp3.MediaType;
import okhttp3.MultipartBody;
import okhttp3.OkHttpClient;
import okhttp3.RequestBody;
import okhttp3.ResponseBody;
import okio.BufferedSink;
cs


Retrofit를 이용해서 비동기통신을 구현할 것입니다. 

Retrofit : http://square.github.io/retrofit/

Retrofit 한글 번역 : http://devflow.github.io/retrofit-kr/


import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;
import retrofit2.http.POST;
cs


2. 레이아웃

레이아웃을 가져옵니다.


    ImageView mGetImageFromGalleryButton;
 
    ImageView mImageview;
    TextView mEditTextdialogOpenField;
    Uri returnUri;
cs


여러가지 View는 inflater를 이용해서 가져옵니다. 앞으로도 view를 가져올 땐 inflater의 inflate 함수를 이용할 것입니다.


        View view = inflater.inflate(R.layout.fragment_green, container, false);
        customImageview = (ImageView) view.findViewById(R.id.iv_beforesendpost);
        customEditTextdialogOpenField = (TextView) view.findViewById(R.id.tv_textToPost);
        setRetainInstance(true);
 
        mEditTextdialogOpenField.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                openDialog();
            }
        });
cs


가져온 뷰에 각기 onClickListener를 달아줍니다. 레이아웃상의 버튼을 클릭하면 일정한 동작이 구현되게 할 수 있습니다.


        view.findViewById(R.id.btn_post).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
            }
        }
cs


이렇게 하게 되면 기본적인 세팅이 완성됩니다.

Activity는 java android 프로그래밍에서 핵심이 되는 부분입니다. 이 부분에서 여러가지 프로그래밍적인 설정을 할 수 있습니다. 레이아웃과의 연동을 위해 우선은 (ImageView), (TextView) 등을 가져온 부분을 주의깊게 봐주시기 바랍니다.


다음 포스트에서 계속하겠습니다.

+ Recent posts